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:
- Une fonction
printTwoParameters
" printTwoParameters
" est printTwoParameters
, mais n'accepte qu'un seul paramètre: "x". - 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.
- Enfin, la fonction locale nouvellement créée est renvoyée.
- 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.