Pensée fonctionnelle. Partie 10

Pouvez-vous imaginer que c'est le dixième du cycle! Bien que le récit se soit auparavant concentré sur un style purement fonctionnel, il est parfois pratique de passer à un style orienté objet. Et l'une des principales caractéristiques d'un style orienté objet est la possibilité d'attacher des fonctions à une classe et d'accéder à la classe via un point pour obtenir le comportement souhaité.






En F #, cela est possible avec une fonctionnalité appelée "extensions de type". Tout type F #, et pas seulement une classe, peut avoir des fonctions attachées.


Voici un exemple d'attachement d'une fonction à un type d'enregistrement.


module Person = type T = {First:string; Last:string} with // -,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName 

Points clés à surveiller:


  • Le mot clé with indique le début d'une liste de membres.
  • Le mot-clé member indique que la fonction est un membre (c'est-à-dire une méthode)
  • Le mot this est l'étiquette de l'objet sur lequel cette méthode est appelée (également appelée "auto-identifiant"). Ce mot est le préfixe du nom de la fonction, et à l'intérieur de la fonction, vous pouvez l'utiliser pour faire référence à l'instance actuelle. Il n'y a aucune exigence pour les mots utilisés comme auto-identifiant, il suffit qu'ils soient stables. Vous pouvez utiliser this , self , me ou tout autre mot qui est généralement utilisé comme référence à soi-même.

Il n'est pas nécessaire d'ajouter un membre avec une déclaration de type, vous pouvez toujours l'ajouter plus tard dans le même module:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //  ,   type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName 

Ces exemples démontrent l'appel aux "extensions intrinsèques". Ils sont compilés en un type et seront disponibles partout où le type est utilisé. Ils seront également affichés lors de l'utilisation de la réflexion.


Les extensions internes vous permettent même de diviser une définition de type en plusieurs fichiers, tant que tous les composants utilisent le même espace de noms et se compilent en un seul assemblage. Comme pour les classes partielles en C #, cela peut être utile pour séparer le code généré et écrit à la main.


Extensions optionnelles


Une alternative consiste à ajouter un membre supplémentaire à partir d'un module complètement différent. Ils sont appelés «extensions facultatives». Ils ne compilent pas à l'intérieur de la classe et nécessitent un module de portée différent pour fonctionner avec eux (ce comportement ressemble aux méthodes d'extension de C #).


Par exemple, laissez un type de Person être défini:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //   ,   type T with member this.SortableName = this.Last + ", " + this.First 

L'exemple ci-dessous montre comment lui ajouter l'extension UppercaseName dans un autre module:


 //    module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper() 

Vous pouvez maintenant essayer cette extension:


 let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Oups, nous obtenons une erreur. Cela s'est produit parce que PersonExtensions pas dans la portée. Comme en C #, pour utiliser des extensions, vous devez les entrer dans la portée.


Une fois cela fait, cela fonctionnera:


 //    ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Extensions de type de système


Vous pouvez également étendre des types à partir de bibliothèques .NET. Mais il ne faut pas oublier que lorsque vous développez un type, vous devez utiliser son nom réel, et non un alias.


Par exemple, si vous essayez de développer int , rien ne fonctionnera, car int pas un nom valide pour le type:


 type int with member this.IsEven = this % 2 = 0 

Utilisez plutôt System.Int32 :


 type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i 

Membres statiques


Vous pouvez créer des fonctions membres statiques en utilisant:


  • ajouter static
  • supprimer this balise

 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //   static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName 

Vous pouvez créer des membres statiques pour les types de système:


 type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi 

Attacher des fonctionnalités existantes


Un modèle très courant est l'attachement de fonctions indépendantes existantes à un type. Il présente plusieurs avantages:


  • Pendant le développement, vous pouvez déclarer des fonctions indépendantes qui font référence à d'autres fonctions indépendantes. Cela simplifiera le développement, car l'inférence de type fonctionne beaucoup mieux avec un style fonctionnel qu'avec un style orienté objet ("point à point").
  • Mais certaines fonctions clés peuvent être attachées à un type. Cela permet aux utilisateurs de choisir lequel des styles utiliser - fonctionnel ou orienté objet.

Un exemple d'une telle solution est une fonction de la bibliothèque F #, qui calcule la longueur de la liste. Vous pouvez utiliser une fonction indépendante du module List ou l'appeler comme méthode d'instance.


 let list = [1..10] //   let len1 = List.length list // -  let len2 = list.Length 

Dans l'exemple suivant, le type n'a initialement aucun membre, puis plusieurs fonctions sont définies, et enfin la fonction fullName est attachée au type.


 module Person = // ,     type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last //       type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person //  let fullname2 = person.FullName //  

La fonction fullName a un paramètre, person . Le membre attaché reçoit le paramètre de l'auto-lien.


Ajout de fonctions existantes avec plusieurs paramètres


Il y a une autre fonctionnalité intéressante. Si la fonction définie précédemment prend plusieurs paramètres, lorsque vous l'attachez au type, vous n'aurez plus à répertorier tous ces paramètres. Il suffit de spécifier d'abord this paramètre.


Dans l'exemple ci-dessous, la fonction hasSameFirstAndLastName a trois paramètres. Cependant, lors de la fixation, il suffit d'en mentionner un seul!


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast //      type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" //  let result2 = person.HasSameFirstAndLastName "bob" "smith" //  

Pourquoi ça marche? Astuce: pensez au curry et à l'utilisation partielle!


Méthodes de tuple


Lorsque nous avons des méthodes avec plus d'un paramètre, vous devez prendre une décision:


  • nous pouvons utiliser la forme standard (curry), où les paramètres sont séparés par des espaces, et une application partielle est prise en charge.
  • ou nous pouvons passer tous les paramètres à la fois sous la forme d'un tuple séparé par des virgules.

La forme curry est plus fonctionnelle, tandis que la forme tuple est plus orientée objet.


Un formulaire de tuple est également utilisé pour interagir avec F # avec les bibliothèques .NET standard, vous devez donc considérer cette approche plus en détail.


Notre site de test sera un type de Product avec deux méthodes, chacune étant mise en œuvre par l'une des méthodes décrites ci-dessus. Les TupleTotal CurriedTotal et TupleTotal font la même chose: elles calculent le coût total du produit pour une quantité et une remise données.


 type Product = {SKU:string; Price: float} with //   member this.CurriedTotal qty discount = (this.Price * float qty) - discount //   member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount 

Code de test:


 let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0) 

Jusqu'à présent, il n'y a pas beaucoup de différence.


Mais nous savons que la version au curry peut être partiellement appliquée:


 let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10 

En revanche, la version tuple est capable de quelque chose qui ne peut pas être curry, à savoir:


  • Paramètres nommés
  • Paramètres facultatifs
  • Surcharge

Paramètres nommés avec des paramètres sous la forme d'un tuple


L'approche tuple prend en charge les paramètres nommés:


 let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10) 

Comme vous pouvez le voir, cela vous permet de modifier l'ordre des arguments en spécifiant explicitement les noms.


Attention: si seuls certains paramètres ont des noms, ces paramètres doivent toujours être à la fin.


Paramètres optionnels avec des paramètres sous forme de tuple


Pour les méthodes avec des paramètres sous la forme d'un tuple, vous pouvez marquer les paramètres comme facultatifs à l'aide du préfixe sous la forme d'un point d'interrogation devant le nom du paramètre.


  • Si le paramètre est défini, une Some value sera transmise à la fonction
  • Sinon, None ne viendra

Un exemple:


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount 

Et le test:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal2(10) //   let total2 = product.TupleTotal2(10,1.0) 

La vérification explicite de None et Some peut être fastidieuse, mais il existe une solution plus élégante pour gérer les paramètres facultatifs.


Il existe une fonction defaultArg qui prend un nom de paramètre comme premier argument et une valeur par défaut comme second. Si le paramètre est défini, la valeur correspondante sera retournée; sinon, la valeur par défaut.


Le même code en utilisant defaulArg :


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount 

Surcharge de méthode


En C #, vous pouvez créer plusieurs méthodes avec le même nom qui diffèrent par leur signature (par exemple, différents types de paramètres et / ou leur nombre).


Dans un modèle purement fonctionnel, cela n'a pas de sens - la fonction fonctionne avec un type spécifique d'argument (domaine) et un type spécifique de valeur de retour (plage). La même fonction ne peut pas interagir avec d'autres domaines et plages.


Cependant, F # prend en charge la surcharge de méthodes, mais uniquement pour les méthodes (qui sont attachées aux types) et uniquement celles qui sont écrites dans un style de tuple.


Voici un exemple avec une autre variante de la méthode TupleTotal !


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty //   member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount 

En règle générale, le compilateur F # jure qu'il existe deux méthodes avec le même nom, mais dans ce cas, cela est acceptable, car ils sont déclarés en notation tuple et leurs signatures sont différentes. (Pour indiquer clairement quelle méthode est appelée, j'ai ajouté de petits messages pour le débogage)


Exemple d'utilisation:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal3(10) //   let total2 = product.TupleTotal3(10,1.0) 

Hé! Pas si vite ... Les inconvénients de l'utilisation des méthodes


Venant d'un monde orienté objet, vous pouvez être tenté d'utiliser des méthodes partout, car c'est quelque chose de familier. Mais vous devez être prudent, car ils présentent un certain nombre d'inconvénients graves:


  • Les méthodes ne fonctionnent pas bien avec l'inférence de type
  • Les méthodes ne fonctionnent pas bien avec des fonctions d'ordre supérieur

En fait, en abusant des méthodes, vous pouvez manquer les aspects les plus puissants et utiles de la programmation en F #.


Voyons ce que je veux dire.


Les méthodes interagissent mal avec l'inférence de type


Revenons à l'exemple avec Person , dans lequel la même logique a été implémentée dans une fonction indépendante et dans une méthode:


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this 

Voyons maintenant dans quelle mesure l'inférence de type fonctionne avec chacune des méthodes. Supposons que je veuille imprimer le nom complet d'une personne, alors je définirai la fonction printFullName , qui prend la person comme paramètre.


Code utilisant une fonction indépendante du module:


 open Person //    let printFullName person = printfn "Name is %s" (fullName person) //    // val printFullName : Person.T -> unit 

Il compile sans problème et l'inférence de type identifie correctement le paramètre comme Person .


Essayez maintenant la version via le point:


 open Person //    " " let printFullName2 person = printfn "Name is %s" (person.FullName) 

Ce code ne compile pas du tout, car l'inférence de type n'a pas suffisamment d'informations pour déterminer le type de paramètre. Tout objet peut implémenter .FullName - cela ne suffit pas pour la sortie.


Oui, nous pouvons annoter une fonction avec un type de paramètre, mais pour cette raison, le point entier d'inférence de type automatique est perdu.


Les méthodes vont mal avec les fonctions d'ordre supérieur


Un problème similaire se pose dans les fonctions d'ordre supérieur. Par exemple, il y a une liste de personnes, et nous devons obtenir une liste de leurs noms complets.


Dans le cas d'une fonction indépendante, la solution est triviale:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //     list |> List.map fullName 

Dans le cas d'une méthode objet, vous devez créer un lambda spécial partout:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //    list |> List.map (fun p -> p.FullName) 

Mais c'est encore un exemple assez simple. Les méthodes des objets se prêtent très bien à la composition, ne conviennent pas dans le pipeline, etc.


Par conséquent, si vous êtes nouveau dans la programmation fonctionnelle, je vous exhorte: si vous le pouvez, n'utilisez pas de méthodes, en particulier dans le processus d'apprentissage. Ce sera une béquille qui ne vous permettra pas de tirer le maximum de profit de la programmation fonctionnelle.


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/fr433410/


All Articles