Pensée fonctionnelle. Partie 5

Dans un article précédent sur le curry, nous avons vu comment les fonctions avec plusieurs paramètres sont divisées en fonctions plus petites, avec un paramètre. Il s'agit d'une solution mathématiquement correcte, mais il existe d'autres raisons de le faire - elle conduit également à une technique très puissante appelée application partielle de fonctions . Ce style est très largement utilisé en programmation fonctionnelle, et il est très important de le comprendre.




Utilisation partielle des fonctions


L'idée d'une application partielle est que si nous fixons les N premiers paramètres de la fonction, nous obtenons une nouvelle fonction avec les paramètres restants. De la discussion sur le curry, on pouvait voir comment une application partielle se produit naturellement.
Quelques exemples simples pour illustrer:


//  ""       +  42 let add42 = (+) 42 //    add42 1 add42 3 //       //      [1;2;3] |> List.map add42 //          "" let twoIsLessThan = (<) 2 //   twoIsLessThan 1 twoIsLessThan 3 //      twoIsLessThan [1;2;3] |> List.filter twoIsLessThan //   ""       printfn let printer = printfn "printing param=%i" //      printer    [1;2;3] |> List.iter printer 

Dans chaque cas, nous créons une fonction partiellement appliquée qui peut être réutilisée dans différentes situations.


Et bien sûr, une application partielle facilite également la fixation des paramètres de fonction. Voici quelques exemples:


 //    List.map let add1 = (+) 1 let add1ToEach = List.map add1 //   "add1"  List.map //  add1ToEach [1;2;3;4] //    List.filter let filterEvens = List.filter (fun i -> i%2 = 0) //    //  filterEvens [1;2;3;4] 

L'exemple suivant, plus complexe, illustre comment la même approche peut être utilisée pour créer de manière transparente un comportement «intégré».


  • Nous créons une fonction qui additionne deux nombres, mais en plus il faut une fonction de journalisation qui enregistrera ces nombres et le résultat.
  • La fonction de journalisation prend deux paramètres: (chaîne) "nom" et (générique) "valeur", elle a donc la string->'a->unit signature string->'a->unit .
  • Ensuite, nous créons différentes implémentations de la fonction de journalisation, comme un enregistreur de console ou un enregistreur contextuel.
  • Et enfin, nous appliquons partiellement la fonction principale pour créer une nouvelle fonction, avec un enregistreur fermé.

 //      - let adderWithPluggableLogger logger xy = logger "x" x logger "y" y let result = x + y logger "x+y" result result //  -      let consoleLogger argName argValue = printfn "%s=%A" argName argValue //           let addWithConsoleLogger = adderWithPluggableLogger consoleLogger addWithConsoleLogger 1 2 addWithConsoleLogger 42 99 //  -      let popupLogger argName argValue = let message = sprintf "%s=%A" argName argValue System.Windows.Forms.MessageBox.Show( text=message,caption="Logger") |> ignore //    -       let addWithPopupLogger = adderWithPluggableLogger popupLogger addWithPopupLogger 1 2 addWithPopupLogger 42 99 

Ces fonctions d'enregistreur fermé peuvent être utilisées comme n'importe quelle autre fonction. Par exemple, nous pouvons créer une application partielle pour ajouter 42, puis la transmettre à la fonction de liste, comme nous l'avons fait pour la simple fonction add42 .


 //         42 let add42WithConsoleLogger = addWithConsoleLogger 42 [1;2;3] |> List.map add42WithConsoleLogger [1;2;3] |> List.map add42 //     

Les fonctions partiellement appliquées sont un outil très utile. Nous pouvons créer des fonctions de bibliothèque flexibles (bien que complexes), et il est facile de les rendre réutilisables par défaut, de sorte que la complexité sera cachée du code client.


Conception de fonctions partielles


De toute évidence, l'ordre des paramètres peut sérieusement affecter la commodité d'une utilisation partielle. Par exemple, la plupart des fonctions de List telles que List.map et List.filter ont une forme similaire, à savoir:


 List-function [function parameter(s)] [list] 

La liste est toujours le dernier paramètre. Quelques exemples en pleine forme:


 List.map (fun i -> i+1) [0;1;2;3] List.filter (fun i -> i>1) [0;1;2;3] List.sortBy (fun i -> -i ) [0;1;2;3] 

Les mêmes exemples utilisant une application partielle:


 let eachAdd1 = List.map (fun i -> i+1) eachAdd1 [0;1;2;3] let excludeOneOrLess = List.filter (fun i -> i>1) excludeOneOrLess [0;1;2;3] let sortDesc = List.sortBy (fun i -> -i) sortDesc [0;1;2;3] 

Si les fonctions de bibliothèque étaient implémentées avec un ordre d'arguments différent, une application partielle serait beaucoup moins pratique.


Lorsque vous écrivez votre fonction avec de nombreux paramètres, vous pouvez penser à leur meilleur ordre. Comme pour tous les problèmes de conception, il n'y a pas de «bonne» réponse, mais il existe plusieurs recommandations généralement acceptées.


  1. Commencez avec des options susceptibles d'être statiques.
  2. Soyez le dernier à définir des structures de données ou des collections (ou d'autres paramètres changeants)
  3. Pour une meilleure compréhension des opérations telles que la soustraction, il est conseillé d'observer l'ordre attendu

Le premier conseil est simple. Les paramètres susceptibles d'être «fixés» par une application partielle doivent être traités en premier, comme dans les exemples avec l'enregistreur ci-dessus.


Le fait de suivre la deuxième astuce facilite l'utilisation de l'opérateur de pipelining et de la composition. Nous l'avons déjà vu à plusieurs reprises dans les exemples de fonctions ci-dessus.


 //          let result = [1..10] |> List.map (fun i -> i+1) |> List.filter (fun i -> i>5) 

De même, les fonctions partielles appliquées sur les listes sont facilement exposées à la composition, car Le paramètre de liste peut être omis:


 let compositeOp = List.map (fun i -> i+1) >> List.filter (fun i -> i>5) let result = compositeOp [1..10] 

Habillage partiel de la fonction BCL


Les fonctions de bibliothèque de classes de base .NET (BCL) de .NET sont facilement accessibles à partir de F #, mais elles sont conçues sans s'appuyer sur des langages fonctionnels tels que F #. Par exemple, la plupart des fonctions nécessitent un paramètre de données au début, tandis qu'en F #, le paramètre de données doit généralement être le dernier.


Cependant, vous pouvez facilement écrire des wrappers pour rendre ces fonctions plus idiomatiques. Dans l'exemple ci-dessous, les fonctions de chaîne .NET sont réécrites afin que la chaîne cible soit utilisée en dernier plutôt qu'en premier:


 //     .NET  let replace oldStr newStr (s:string) = s.Replace(oldValue=oldStr, newValue=newStr) let startsWith lookFor (s:string) = s.StartsWith(lookFor) 

Une fois que la chaîne est devenue le dernier paramètre, vous pouvez utiliser ces fonctions dans les pipelines, comme d'habitude:


 let result = "hello" |> replace "h" "j" |> startsWith "j" ["the"; "quick"; "brown"; "fox"] |> List.filter (startsWith "f") 

ou dans la composition des fonctions:


 let compositeOp = replace "h" "j" >> startsWith "j" let result = compositeOp "hello" 

Comprendre l'opérateur du convoyeur


Après avoir vu une application partielle dans les affaires, vous pouvez comprendre le fonctionnement des fonctions en pipeline.


La fonction de pipelining est définie comme suit:


 let (|>) xf = fx 

Tout ce qu'elle fait, c'est mettre un argument avant la fonction, pas après.


 let doSomething xyz = x+y+z doSomething 1 2 3 //     

Dans le cas où la fonction f a plusieurs paramètres, et le dernier paramètre de la fonction f agira comme la valeur d'entrée x pipeline. En fait, la fonction transférée f a déjà été partiellement appliquée et n'attend qu'un seul paramètre - la valeur d'entrée pour le pipeline (te x ).


Voici un exemple similaire réécrit pour une utilisation partielle.


 let doSomething xy = let intermediateFn z = x+y+z intermediateFn //  intermediateFn let doSomethingPartial = doSomething 1 2 doSomethingPartial 3 //       3 |> doSomethingPartial //    ,        

Comme vous l'avez vu, l'opérateur pipelined est extrêmement courant en F # et est utilisé chaque fois que vous souhaitez préserver le flux naturel de données. Quelques exemples supplémentaires que vous avez pu rencontrer:


 "12" |> int //   "12"  int 1 |> (+) 2 |> (*) 3 //   

Opérateur de convoyeur inversé


De temps en temps, vous pouvez rencontrer l'opérateur de pipeline inverse "<|".


 let (<|) fx = fx 

Cette fonction semble ne rien faire, alors pourquoi existe-t-elle?


La raison en est que lorsque l'opérateur de pipeline inverse est utilisé comme un opérateur binaire de style infixe, il réduit le besoin de parenthèses, ce qui rend le code plus propre.


 printf "%i" 1+2 //  printf "%i" (1+2) //   printf "%i" <| 1+2 //    

Vous pouvez utiliser des pipelines dans deux directions à la fois pour obtenir une notation pseudo-infixée.


 let add xy = x + y (1+2) add (3+4) //  1+2 |> add <| 3+4 //   

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


All Articles