Pensée fonctionnelle. Partie 4

Après une courte digression dans les types de base, nous pouvons à nouveau revenir aux fonctions. En particulier, à l'énigme mentionnée précédemment: si une fonction mathématique ne peut prendre qu'un seul paramètre, alors comment une fonction en F # peut-elle prendre plus de paramètres? Plus de détails sous la coupe!




La réponse est assez simple: une fonction à plusieurs paramètres est réécrite sous la forme d'une série de nouvelles fonctions, dont chacune ne prend qu'un seul paramètre. Le compilateur effectue cette opération automatiquement, et elle est appelée " currying ", en l'honneur de Haskell Curry, un mathématicien qui a considérablement influencé le développement de la programmation fonctionnelle.


Pour voir comment fonctionne le curry dans la pratique, utilisons un exemple de code simple qui imprime deux nombres:


//   let printTwoParameters xy = printfn "x=%iy=%i" xy 

En fait, le compilateur le réécrit approximativement sous la forme suivante:


 //    let printTwoParameters x = //    let subFunction y = printfn "x=%iy=%i" xy //  ,    subFunction //   

Considérez ce processus plus en détail:


  1. Une fonction printTwoParameters " printTwoParameters " est printTwoParameters , mais n'accepte qu'un seul paramètre: "x".
  2. Une fonction locale est créée à l'intérieur, qui ne prend également qu'un seul paramètre: "y". Notez que la fonction locale utilise le paramètre "x", mais x ne lui est pas passé comme argument. "x" est dans une telle portée qu'une fonction imbriquée peut la voir et l'utiliser sans avoir besoin de la passer.
  3. Enfin, la fonction locale nouvellement créée est renvoyée.
  4. La fonction retournée est ensuite appliquée à l'argument "y". Le paramètre "x" y est fermé de sorte que la fonction retournée n'a besoin que du paramètre "y" pour compléter sa logique.

En réécrivant les fonctions de cette manière, le compilateur s'assure que chaque fonction n'accepte qu'un seul paramètre, comme requis. Ainsi, en utilisant " printTwoParameters ", vous pourriez penser qu'il s'agit d'une fonction avec deux paramètres, mais en fait une fonction avec un seul paramètre est utilisée. Vous pouvez le vérifier en lui passant un seul argument au lieu de deux:


 //     printTwoParameters 1 //    val it : (int -> unit) = <fun:printTwoParameters@286-3> 

Si nous le calculons avec un seul argument, nous n'obtiendrons pas d'erreur - la fonction sera retournée.


Voici donc ce qui se passe réellement lorsque printTwoParameters est appelé avec deux arguments:


  • printTwoParameters est printTwoParameters avec le premier argument (x)
  • printTwoParameters renvoie une nouvelle fonction dans laquelle "x" est fermé.
  • Ensuite, une nouvelle fonction est appelée avec le deuxième argument (y)

Voici un exemple de versions pas à pas et normales:


 //   let x = 6 let y = 99 let intermediateFn = printTwoParameters x //  -  // x   let result = intermediateFn y //     let result = (printTwoParameters x) y //   let result = printTwoParameters xy 

Voici un autre exemple:


 //  let addTwoParameters xy = x + y //   let addTwoParameters x = //   ! let subFunction y = x + y //      subFunction //   //       let x = 6 let y = 99 let intermediateFn = addTwoParameters x //  -  // x   let result = intermediateFn y //   let result = addTwoParameters xy 

Encore une fois, une «fonction à deux paramètres» est en fait une fonction à un paramètre, qui renvoie une fonction intermédiaire.


Mais attendez, qu'en est-il de l'opérateur + ? Est-ce une opération binaire qui doit prendre deux paramètres? Non, il est également curry, comme les autres fonctions. Il s'agit d'une fonction appelée " + " qui prend un paramètre et renvoie une nouvelle fonction intermédiaire, tout comme addTwoParameters ci-dessus.


Lorsque nous écrivons l'expression x+y , le compilateur réorganise le code de manière à convertir l'infixe en (+) xy , qui est une fonction nommée + qui prend deux paramètres. Notez que la fonction "+" a besoin de parenthèses pour indiquer qu'elle est utilisée comme une fonction régulière, et non comme un opérateur infixe.


Enfin, une fonction à deux paramètres, appelée + , est traitée comme toute autre fonction à deux paramètres.


 //         let x = 6 let y = 99 let intermediateFn = (+) x //   ""  ""   let result = intermediateFn y //        let result = (+) xy //       let result = x + y 

Et oui, cela fonctionne pour tous les autres opérateurs et fonctions intégrées comme printf .


 //    let result = 3 * 5 //    - let intermediateFn = (*) 3 //  ""  3   let result = intermediateFn 5 //    printfn let result = printfn "x=%iy=%i" 3 5 // printfn   - let intermediateFn = printfn "x=%iy=%i" 3 // "3"   let result = intermediateFn 5 

Signatures de fonction au curry


Maintenant que nous savons comment fonctionnent les fonctions au curry, il est intéressant de savoir à quoi ressembleront leurs signatures.


En revenant au premier exemple, " printTwoParameter ", nous avons vu que la fonction prenait un argument et printTwoParameter une fonction intermédiaire. La fonction intermédiaire a également pris un argument et n'a rien renvoyé (c'est-à-dire l' unit ). Par conséquent, la fonction intermédiaire était de type int->unit . En d'autres termes, le domaine printTwoParameters est int et la plage est int->unit . Ensemble, nous verrons la signature finale:


 val printTwoParameters : int -> (int -> unit) 

Si vous calculez une implémentation explicitement curry, vous pouvez voir les crochets dans la signature, mais si vous calculez une implémentation ordinaire, implicitement curry, il n'y aura pas de crochets:


 val printTwoParameters : int -> int -> unit 

Les supports sont facultatifs. Mais ils peuvent être représentés dans l'esprit pour simplifier la perception des signatures de fonction.


Et quelle est la différence entre une fonction qui renvoie une fonction intermédiaire et une fonction régulière avec deux paramètres?


Voici une fonction avec un paramètre qui renvoie une autre fonction:


 let add1Param x = (+) x // signature is = int -> (int -> int) 

Et voici une fonction avec deux paramètres qui renvoie une valeur simple:


 let add2Params xy = (+) xy // signature is = int -> int -> int 

Leurs signatures sont légèrement différentes, mais dans un sens pratique, il n'y a pas beaucoup de différence entre elles, à l'exception du fait que la deuxième fonction est automatiquement curry.


Fonctions avec plus de deux paramètres


Comment fonctionne le curry pour les fonctions avec plus de deux paramètres? De la même manière: pour chaque paramètre, sauf le dernier, la fonction retourne une fonction intermédiaire qui ferme le paramètre précédent.


Considérez cet exemple difficile. J'ai explicitement déclaré des types de paramètres, mais la fonction ne fait rien.


 let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () //   let intermediateFn1 = multiParamFn 42 // multoParamFn  int   (bool -> string -> float -> unit) // intermediateFn1  bool //   (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2  string //   (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float //     (unit) let finalResult = intermediateFn3 3.141 

Signature de l'ensemble de la fonction:


 val multiParamFn : int -> bool -> string -> float -> unit 

et signatures de fonctions intermédiaires:


 val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = () 

La signature de la fonction peut vous dire combien de paramètres la fonction prend: il suffit de compter le nombre de flèches en dehors des crochets. Si la fonction accepte ou renvoie une autre fonction, il y aura plus de flèches, mais elles seront entre crochets et elles peuvent être ignorées. Voici quelques exemples:


 int->int->int // 2  int  int string->bool->int //   string,  - bool, //  int int->string->bool->unit //   (int,string,bool) //    (unit) (int->string)->int //   ,  // ( int  string) //   int (int->string)->(int->bool) //   (int  string) //   (int  bool) 

Difficultés de paramètres multiples


Jusqu'à ce que vous compreniez la logique du curry, cela produira des résultats inattendus. N'oubliez pas que vous n'obtiendrez pas d'erreur si vous exécutez la fonction avec moins d'arguments que prévu. Au lieu de cela, vous obtenez une fonction partiellement appliquée. Si vous utilisez ensuite la fonction partiellement appliquée dans le contexte où la valeur est attendue, vous pouvez obtenir une erreur obscure du compilateur.


Prenons une fonction inoffensive à première vue:


 //   let printHello() = printfn "hello" 

Que pensez-vous qu'il se passera si vous l'invoquez comme indiqué ci-dessous? Est-ce que "bonjour" s'imprime sur la console? Essayez de deviner avant l'exécution. Astuce: regardez la signature de la fonction.


 //   printHello 

Contrairement aux attentes, il n'y aura pas d' appel. La fonction d'origine attend l' unit comme un argument qui n'a pas été transmis. Par conséquent, une fonction partiellement appliquée a été obtenue (dans ce cas, sans arguments).


Et cette affaire? Sera-t-il compilé?


 let addXY xy = printfn "x=%iy=%i" x x + y 

Si vous l'exécutez, le compilateur se plaindra de la ligne avec printfn .


 printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit. 

S'il n'y a aucune compréhension du curry, ce message peut être très cryptique. Le fait est que toutes les expressions qui sont évaluées séparément (c'est-à-dire qui ne sont pas utilisées comme valeur de retour ou sont liées à quelque chose au moyen de "let") doivent être évaluées dans la valeur unit . Dans ce cas, il n'est pas calculé dans la valeur unit , mais renvoie à la place une fonction. C'est un long chemin pour dire qu'il manque un argument à printfn .


Dans la plupart des cas, des erreurs comme celle-ci se produisent lors de l'interaction avec une bibliothèque du monde .NET. Par exemple, la méthode Readline de la classe TextReader doit prendre un paramètre d' unit . Vous pouvez souvent l'oublier et ne pas mettre de crochets, dans ce cas, vous ne pouvez pas obtenir une erreur de compilation au moment de l '«appel», mais elle apparaîtra lorsque vous essayez d'interpréter le résultat comme une chaîne.


 let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // ,    printfn "The line is %s" line1 //    // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //   

Dans le code ci-dessus, line1 est juste un pointeur ou un délégué à la méthode Readline , pas une chaîne, comme vous pouvez vous y attendre. L'utilisation de () dans reader.ReadLine() appellera en fait la fonction.


Trop d'options


Vous pouvez obtenir des messages tout aussi cryptiques si vous passez trop de paramètres à une fonction. Quelques exemples de passage de trop de paramètres à printf :


 printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit 

Par exemple, dans ce dernier cas, le compilateur signale qu'une chaîne de format avec trois paramètres est attendue (la signature 'a -> 'b -> 'c -> 'd a trois paramètres), mais à la place, une chaîne avec deux est reçue (pour la signature 'a -> 'b -> unit deux paramètres).


Dans les cas où printf n'est pas utilisé, le passage d'un grand nombre de paramètres signifie souvent qu'à une certaine étape du calcul, une valeur simple a été obtenue, à laquelle le paramètre est essayé. Le compilateur ne comprendra pas qu'une valeur simple n'est pas une fonction.


 let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied 

Si nous décomposons l'appel général en une série de fonctions intermédiaires explicites, comme nous l'avons fait précédemment, nous pouvons voir exactement ce qui ne va pas.


 let add1 x = x + 1 let intermediateFn = add1 2 //   let x = intermediateFn 3 //intermediateFn  ! // ==> error FS0003: This value is not a function // and cannot be applied 

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


All Articles