Pensée fonctionnelle. Partie 9

C'est déjà la partie 9 d'une série d'articles sur la programmation fonctionnelle en F #! Je suis sûr que sur Habré, il n'y a pas beaucoup de cycles aussi longs. Mais nous n'allons pas nous arrêter. Aujourd'hui, nous allons parler des fonctions imbriquées, des modules, des espaces de noms et du mélange des types et des fonctions dans les modules.






Vous savez maintenant comment définir des fonctions, mais comment les organiser?


F # a trois options:


  • les fonctions peuvent être imbriquées dans d'autres fonctions.
  • au niveau de l'application, les fonctions de niveau supérieur sont regroupées en «modules».
  • ou vous pouvez suivre une approche orientée objet et attacher des fonctions aux types comme méthodes.

Dans cet article, nous examinerons les deux premières méthodes et la dernière dans la suivante.


Fonctions imbriquées


En F #, vous pouvez définir des fonctions à l'intérieur d'autres fonctions. C'est un bon moyen d'encapsuler des fonctions auxiliaires qui ne sont nécessaires que pour la fonction principale et ne doivent pas être visibles de l'extérieur.


Dans l'exemple ci-dessous, add imbriqué dans addThreeNumbers :


 let addThreeNumbers xyz = //     let add n = fun x -> x + n //    x |> add y |> add z addThreeNumbers 2 3 4 

Les fonctions imbriquées peuvent accéder directement aux paramètres parents, car elles sont dans sa portée.
Ainsi, dans l'exemple ci-dessous, la fonction imbriquée printError pas besoin de paramètres, car elle peut accéder directement à n et max .


 let validateSize max n = //       let printError() = printfn "Oops: '%i' is bigger than max: '%i'" n max //    if n > max then printError() validateSize 10 9 validateSize 10 11 

Un modèle très courant est la fonction principale qui définit la fonction d'aide récursive imbriquée, qui est appelée avec les valeurs initiales correspondantes.
Voici un exemple d'un tel code:


 let sumNumbersUpTo max = //      let rec recursiveSum n sumSoFar = match n with | 0 -> sumSoFar | _ -> recursiveSum (n-1) (n+sumSoFar) //       recursiveSum max 0 sumNumbersUpTo 10 

Essayez d'éviter l'imbrication profonde, en particulier dans les cas d'accès direct (pas sous forme de paramètres) aux variables parentes.
Les fonctions imbriquées trop profondément seront aussi difficiles à comprendre que la pire de nombreuses branches impératives imbriquées.


Un exemple de comment ne pas faire:


 // wtf,    ? let fx = let f2 y = let f3 z = x * z let f4 z = let f5 z = y * z let f6 () = y * x f6() f4 y x * f2 x 

Modules


Un module est simplement un ensemble de fonctions regroupées, généralement parce qu'elles fonctionnent avec le ou les mêmes types de données.


Une définition de module est très similaire à une définition de fonction. Il commence par le mot-clé module , puis vient le signe = , suivi du contenu du module.
Le contenu du module doit être formaté avec un décalage, ainsi que les expressions dans la définition des fonctions.


Définition d'un module contenant deux fonctions:


 module MathStuff = let add xy = x + y let subtract xy = x - y 

Si vous ouvrez ce code dans Visual Studio, lorsque vous passez la souris sur add vous pouvez voir le nom complet add , qui est en fait MathStuff.add , comme si MastStuff était une classe et add était une méthode.


En fait, c'est exactement ce qui se passe. Dans les coulisses, le compilateur F # crée une classe statique avec des méthodes statiques. L'équivalent C # ressemblerait à ceci:


 static class MathStuff { static public int add(int x, int y) { return x + y; } static public int subtract(int x, int y) { return x - y; } } 

Reconnaître que les modules ne sont que des classes statiques et que les fonctions sont des méthodes statiques donnera une bonne compréhension du fonctionnement des modules en F #, car la plupart des règles qui s'appliquent aux classes statiques s'appliquent également aux modules.


Et tout comme en C #, chaque fonction autonome doit faire partie de la classe, en F #, chaque fonction autonome doit faire partie du module.


Accès aux fonctions en dehors du module


Si vous devez accéder à une fonction à partir d'un autre module, vous pouvez vous y référer par son nom complet.


 module MathStuff = let add xy = x + y let subtract xy = x - y module OtherStuff = //     MathStuff let add1 x = MathStuff.add x 1 

Vous pouvez également importer toutes les fonctions d'un autre module à l'aide de la directive open , après quoi vous pouvez utiliser le nom court au lieu du nom complet.


 module OtherStuff = open MathStuff //      let add1 x = add x 1 

Les règles d'utilisation des noms sont très attendues. Vous pouvez toujours accéder à une fonction par son nom complet, ou vous pouvez utiliser des noms relatifs ou incomplets selon l'étendue actuelle.


Modules imbriqués


Comme les classes statiques, les modules peuvent contenir des modules imbriqués:


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

D'autres modules peuvent faire référence à des fonctions dans des modules imbriqués en utilisant le nom complet ou relatif, selon le cas:


 module OtherStuff = open MathStuff let add1 x = add x 1 //   let add1Float x = MathStuff.FloatLib.add x 1.0 //   let sub1Float x = FloatLib.subtract x 1.0 

Modules de haut niveau


Ainsi, puisque les modules peuvent être imbriqués, par conséquent, en remontant la chaîne, vous pouvez atteindre un module parent de niveau supérieur. Ça l'est vraiment.


Les modules de niveau supérieur sont définis différemment, contrairement aux modules présentés précédemment.


  • La module MyModuleName doit être la première déclaration du fichier
  • Signe = manquant
  • Le contenu du module ne doit pas être en retrait

En général, une déclaration de «niveau supérieur» doit exister dans chaque fichier .FS source. Il y a quelques exceptions, mais c'est toujours une bonne pratique. Le nom du module ne doit pas nécessairement correspondre au nom du fichier, mais deux fichiers ne peuvent pas contenir de modules du même nom.


Pour les fichiers .FSX , la déclaration de module n'est pas nécessaire, dans ce cas, le nom du fichier de script devient automatiquement le nom du module.


Un exemple d'un MathStuff déclaré en tant que module "top module":


 //    module MathStuff let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Notez qu'il n'y a pas de retrait dans le code «de niveau supérieur» ( module MathStuff ), tandis que le contenu du module FloatLib imbriqué FloatLib encore être mis en retrait.


Autres contenus du module


En plus des fonctions, les modules peuvent contenir d'autres déclarations, telles que des déclarations de type, des valeurs simples et du code d'initialisation (par exemple, des constructeurs statiques)


 module MathStuff = //  let add xy = x + y let subtract xy = x - y //   type Complex = {r:float; i:float} type IntegerFunction = int -> int -> int type DegreesOrRadians = Deg | Rad // "" let PI = 3.141 // "" let mutable TrigType = Deg //  /   do printfn "module initialized" 

Soit dit en passant, si vous exécutez ces exemples de manière interactive, vous devrez peut-être redémarrer la session suffisamment souvent pour que le code reste «frais» et ne soit pas infecté par les calculs précédents.


Dissimulation (chevauchement, ombrage)


Ceci est encore notre exemple de module. Notez que MathStuff contient la fonction d' add ainsi que FloatLib .


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Que se passe-t-il si vous ouvrez les deux modules dans la portée actuelle et appelez add ?


 open MathStuff open MathStuff.FloatLib let result = add 1 2 // Compiler error: This expression was expected to // have type float but here has type int 

Et il est arrivé que le module MathStuff.FloatLib redéfinisse le MathStuff origine, qui était bloqué (masqué) par le module FloatLib .


Par conséquent, nous obtenons l'erreur du compilateur FS0001, car le premier paramètre 1 était attendu comme un flottant. Pour résoudre ce problème, vous devez remplacer 1 par 1.0 .


Malheureusement, en pratique, cela est discrètement et facilement ignoré. Parfois, en utilisant cette technique, vous pouvez effectuer des astuces intéressantes, presque comme des sous-classes, mais le plus souvent, la présence de fonctions du même nom est gênante (par exemple, dans le cas de la fonction de map extrêmement courante).


Si vous souhaitez éviter ce comportement, il existe un moyen de l'arrêter avec l'attribut RequireQualifiedAccess . Le même exemple dans lequel les deux modules sont décorés avec cet attribut:


 [<RequireQualifiedAccess>] module MathStuff = let add xy = x + y let subtract xy = x - y //   [<RequireQualifiedAccess>] module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Maintenant, la directive open n'est pas disponible:


 open MathStuff //  open MathStuff.FloatLib //  

Mais vous pouvez toujours accéder aux fonctions (sans aucune ambiguïté) via leurs noms complets:


 let result = MathStuff.add 1 2 let result = MathStuff.FloatLib.add 1.0 2.0 

Contrôle d'accès


F # prend en charge l'utilisation d'opérateurs de contrôle d'accès .NET standard tels que public , private et internal . L'article MSDN contient des informations complètes.


  • Ces spécificateurs d'accès peuvent être appliqués aux fonctions, valeurs, types et autres déclarations de niveau supérieur ("let bound") d'un module. Ils peuvent également être spécifiés pour les modules eux-mêmes (par exemple, un module imbriqué privé peut être nécessaire).
  • Par défaut, tout a un accès public (à l'exception de plusieurs cas), donc pour les protéger, vous devrez utiliser private ou internal .

Ces spécificateurs d'accès ne sont qu'un moyen de contrôler la visibilité en F #. Une manière complètement différente consiste à utiliser des fichiers de signature qui ressemblent à des fichiers d'en-tête C. Ils décrivent de manière abstraite le contenu du module. Les signatures sont très utiles pour une encapsulation sérieuse, mais pour considérer leurs capacités, vous devrez attendre la série prévue sur l'encapsulation et la sécurité basée sur les capacités .


Espaces de noms


Les espaces de noms en F # sont similaires aux espaces de noms de C #. Ils peuvent être utilisés pour organiser les modules et les types afin d'éviter les conflits de noms.


Un espace de noms déclaré à l'aide du mot clé namespace :


 namespace Utilities module MathStuff = //  let add xy = x + y let subtract xy = x - y 

En raison de cet espace de noms, le nom complet du module MathStuff devenu Utilities.MathStuff et le nom complet add est Utilities.MathStuff.add .


Les mêmes règles d'indentation s'appliquent aux modules dans un espace de noms qui ont été montrés ci-dessus pour les modules.


Vous pouvez également déclarer un espace de noms explicitement en ajoutant un point dans le nom du module. C'est-à-dire Le code ci-dessus peut être réécrit comme ceci:


 module Utilities.MathStuff //  let add xy = x + y let subtract xy = x - y 

Le nom complet du module MathStuff est toujours Utilities.MathStuff , mais maintenant c'est un module de niveau supérieur et son contenu n'a pas besoin de retrait.


Quelques fonctionnalités supplémentaires pour l'utilisation des espaces de noms:


  • Les espaces de noms sont facultatifs pour les modules. Contrairement à C #, pour les projets F #, il n'y a pas d'espace de noms par défaut, donc un module de niveau supérieur sans espace de noms sera global. Si vous prévoyez de créer des bibliothèques réutilisables, vous devez ajouter plusieurs espaces de noms pour éviter les conflits avec le code des autres bibliothèques.
  • Les espaces de noms peuvent contenir directement des déclarations de type, mais pas des déclarations de fonction. Comme indiqué précédemment, toutes les déclarations de fonctions et de valeurs doivent faire partie d'un module.
  • Enfin, gardez à l'esprit que les espaces de noms ne fonctionnent pas dans les scripts. Par exemple, si vous essayez d'envoyer une déclaration d'espace de noms, telle que namespace Utilities de namespace Utilities , à une fenêtre interactive, une erreur est reçue.

Hiérarchie des espaces de noms


Vous pouvez créer une hiérarchie d'espaces de noms en divisant simplement les noms par des points:


 namespace Core.Utilities module MathStuff = let add xy = x + y 

Vous pouvez également déclarer deux espaces de noms dans un fichier si vous le souhaitez. Il convient de noter que tous les espaces de noms doivent être déclarés par leur nom complet - ils ne prennent pas en charge l'imbrication.


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core.Extra module MoreMathStuff = let add xy = x + y 

Un conflit de nom entre l'espace de noms et le module n'est pas possible.


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core //    - Core.Utilities //     ! module Utilities = let add xy = x + y 

Mélanger les types et les fonctions dans les modules


Comme nous l'avons vu, les modules se composent généralement de nombreuses fonctions interdépendantes qui interagissent avec un type de données particulier.


Dans la POO, les structures de données et les fonctions au-dessus d'eux seraient combinées au sein d'une classe. Et en F # fonctionnel, les structures de données et les fonctions au-dessus sont combinées dans un module.


Il existe deux modèles pour combiner des types et des fonctions:


  • le type est déclaré séparément des fonctions
  • le type est déclaré dans le même module que les fonctions

Dans le premier cas, le type est déclaré en dehors de tout module (mais dans l'espace de noms), après quoi les fonctions qui fonctionnent avec ce type sont placées dans le module du même type.


 //    namespace Example //      type PersonType = {First:string; Last:string} //    ,     module Person = //  let create first last = {First=first; Last=last} // ,     let fullName {First=first; Last=last} = first + " " + last let person = Person.create "john" "doe" Person.fullName person |> printfn "Fullname=%s" 

Alternativement, le type est déclaré à l'intérieur du module et a un nom simple tel que " T " ou le nom du module. L'accès aux fonctions est approximativement le suivant: MyModule.Func et MyModule.Func2 , et l'accès au type: MyModule.T :


 module Customer = // Customer.T -      type T = {AccountId:int; Name:string} //  let create id name = {T.AccountId=id; T.Name=name} // ,     let isValid {T.AccountId=id; } = id > 0 let customer = Customer.create 42 "bob" Customer.isValid customer |> printfn "Is valid?=%b" 

Notez que dans les deux cas, il doit y avoir une fonction constructeur qui crée une nouvelle instance du type (usine). Ensuite, dans le code client, vous n'avez pratiquement pas à accéder explicitement au nom du type, et vous n'aurez pas à vous demander si le type est à l'intérieur du module ou non.


Alors quelle voie choisir?


  • La première approche ressemble plus à .NET classique et devrait être préférée si vous prévoyez d'utiliser cette bibliothèque pour du code en dehors de F #, où une classe existante séparément est attendue.
  • La deuxième approche est plus courante dans d'autres langages fonctionnels. Le type à l'intérieur du module se compile comme une classe imbriquée, ce qui n'est généralement pas très pratique pour les langages OOP.

Pour vous-même, vous pouvez expérimenter les deux méthodes. Dans le cas du développement d'équipe, un style doit être choisi.


Modules contenant uniquement des types


S'il existe de nombreux types qui doivent être déclarés sans aucune fonction, ne vous embêtez pas à utiliser le module. Vous pouvez déclarer des types directement dans l'espace de noms sans recourir à des classes imbriquées.


Par exemple, vous pourriez vouloir faire ceci:


 //    module Example //     type PersonType = {First:string; Last:string} //    ,  ... 

Et voici une autre façon de faire de même. Le module mots module simplement remplacé par l' namespace mots.


 //    namespace Example //     type PersonType = {First:string; Last:string} 

Dans les deux cas, PersonType aura le même nom complet.


Veuillez noter que ce remplacement ne fonctionne qu'avec les types. Les fonctions doivent toujours être déclarées à l'intérieur du module.


Ressources supplémentaires


Il existe de nombreux didacticiels pour F #, y compris des documents pour ceux qui viennent avec une expérience C # ou Java. Les liens suivants peuvent être utiles pour approfondir F #:



Plusieurs autres façons de commencer à apprendre le F # sont également décrites.


Enfin, la communauté F # est très conviviale pour les débutants. Il y a un chat très actif chez Slack, soutenu par la F # Software Foundation, avec des salles pour débutants que vous pouvez rejoindre librement . Nous vous recommandons fortement de le faire!


N'oubliez pas de visiter le site de la communauté russophone F # ! Si vous avez des questions sur l'apprentissage d'une langue, nous serons heureux d'en discuter dans les salles de chat:



À propos des auteurs de traduction


Traduit par @kleidemos
La traduction et les modifications éditoriales ont été apportées par les efforts de la communauté russophone des développeurs F # . Nous remercions également @schvepsss et @shwars d' avoir préparé cet article pour publication.

Source: https://habr.com/ru/post/fr433406/


All Articles