L04 - Fonctions anonymes, application partielle et composition
By Laurent Le Brun on Saturday, November 10 2007, 22:57 - [FR] Cours F# - Permalink
Les fonctions anonymes, pillier de la programmation fonctionnelle, sont expliquées dans cet article. D'autres concepts importants sont également détaillés : l'application partielle de fonction et les opérateurs manipulant des fonctions.
Fonctions anonymes
On appelle fonction anonyme une fonction qui n'a pas de nom. De même que l'on utilise souvent des entiers ou des chaînes de caractères sans vouloir les nommer, on peut souhaiter avoir une fonction (souvent courte) sans lui donner de nom. La syntaxe des fonctions anonymes est : fun <arg1> <argn> -> <expression>
[fsharp] > fun x -> x + 1;; val it : int -> int = <fun:clo@0_3>
est une fonction anonyme qui ajoute 1 à son argument. Elle peut s'utiliser partout où une fonction classique peut être appelée.
[fsharp] > (fun x -> x + 1) 5;; val it : int = 6
Une fonction anonyme est une expression comme une autre.
[fsharp] > if 4 < 5 then fun x -> x + 1 else fun x -> x - 1;; val it : (int -> int) = <fun:it@18> > (if 4 < 5 then fun x -> x + 1 else fun x -> x - 1) 6;; val it : int = 7
Les deux définitions suivantes sont donc équivalentes :
[fsharp] > let next = fun x -> x + 1;; val next : int -> int > let next' x = x + 1;; val next' : int -> int
On peut voir la deuxième définition comme étant du sucre syntaxique (c'est-à-dire, un raccourci) pour la première.
Application partielle
Regardons la définition suivante :
[fsharp] > let add x = fun y -> x + y;; val add : int -> int -> int
La fonction a pour type int -> int -> int. La flèche est associative à droite, le type peut aussi s'écrire : int -> (int -> int). Une fonction qui prend un argument un entier et renvoie une fonction de type int -> int peut aussi être vue comme une fonction qui prend deux entiers en argument et renvoie un entier.
[fsharp] > let add x y = x + y;; // ce code est équivalent au précédent val add : int -> int -> int
Quelques exemples :
[fsharp] > let add' = add 4;; val add' : (int -> int) > add' 5;; val it : int = 9 > add 4 5;; val it : int = 9
D'une manière générale, toute fonction qui prend n arguments peut être appelée avec k arguments, k < n. Le résultat est une fonction qui prend k - n arguments. Cela s'appelle l'application partielle.
Voici un autre exemple avec les fonctions min et max, toutes deux définies dans la bibliothèque standard.
[fsharp] > let m = min 4;; val m : (int -> int) > m 6;; val it : int = 4 > m 2;; val it : int = 2
Les définitions suivantes sont équivalentes :
[fsharp] > let add x y = x + y;; val add : int -> int -> int > let add x = fun y -> x + y;; val add : int -> int -> int > let add = fun x -> fun y -> x + y;; val add : int -> int -> int > let add = fun x y -> x + y;; val add : int -> int -> int
Opérateurs
Si l'on a besoin d'utiliser un opérateur comme une expression classique, il suffit de le mettre en parenthèses :
[fsharp] > (+);; val it : (int -> int -> int) = <fun:it@46>
Un opérateur étant une fonction, on peut l'utiliser en notation préfixe (pour les amateurs de Lisp) :
[fsharp] > (+) 4 6;; val it : int = 10
Ou utiliser l'application partielle :
[fsharp] > (+) 4;; val it : (int -> int) = <fun:it@47>
On constate aussi que (+) est le nom de l'opérateur, c'est en effet un identifiant comme un autre. Il est donc possible, pour s'amuser, de redéfinir l'opérateur (+).
On redéfinit (+) localement :
[fsharp] > let (+) x y = x - y in 3 + 4;; val it : int = -1 > let (-) = (+) in 5 - 3;; val it : int = 8
Plus utile, et moins dangereux, on peut définir ses propres opérateurs :
[fsharp] > let (--) x y = abs (x - y);; val ( -- ) : int -> int -> int > 3 -- 5;; val it : int = 2
Les règles pour définir ses opérateurs sont un peu compliquées (concernant les noms valides, les priorités, la notion de préfixe/infixe), mais voici quelques exemples plus ou moins utiles :
[fsharp] > let (@-@) x y = x + " " + y;; val ( @-@ ) : string -> string -> string > "hello" @-@ "world";; val it : string = "hello world" > let (+) = 42 in 4 - (+);; val it : int = -38
Comments
Comment indiqué, (+) a le profil (int -> int -> int)
Pourquoi ce profil n'est-il pas polymorphe? ('a-> 'a-> 'a)
(L'expression (+) 4.1 5.1;; est pourtant correcte!)
L'opérateur (+) est surchargé. Il accepte en argument les types ayant la méthode adéquate (par défaut, les types int, float, string...), mais pas les autres. Le mécanisme de surcharge est un peu complexe, il y a des subtilités et je ne souhaitais pas entrer dans les détails sur cette page.
Si le type (+) avait le type 'a -> 'a -> 'a, il serait trop générique et il serait alors autorisé d'additionner deux caractères, ou même deux fonctions. Ce que l'on ne veut pas.
Ce qu'il faut retenir, c'est que l'inférence de type essaie de trouver quels types sont utilisés. Si on écrit "x + 1.2", alors x doit être de type float. Cependant, quand il n'y a absolument aucune information de typage, le compilateur ne peut pas deviner le type. Dans ce cas (qui arrive très rarement dans un projet, puisque le compilateur a accès au contexte et voit comment la fonction est utilisée), le type int est utilisé par défaut.
je note juste qu'une fonction anonyme est une fonction qui n'a pas de nom, je ne comprenais pas en effet la différence avec la fonction.
fonction : let ajout x = x +1;; ajout 2 ;;
fonction anonyme : ( fun x->x+1 ) 2;;
j'espère que mon commentaire pourra aider quelqu'un.
Merci pour le retour, je viens de mettre à jour l'article.
merci pour ton cours, j'étudie les notions que tu proposes. aujourdhui, j'ai écrit un article sur l'application partielle qui est expliquée aussi dans le livre expert f#.
blog.developpez.com/ylarv...
j'espère que cela te permettra d'améliorer ton cours trés propre par ailleurs.
Bonjour,
Voici une question sur les fonctions anonymes et l'inférence de type:
Pourquoi l'expression suivante (un peu artificielle il est vrai) ne compile-t-elle pas ?
let test = List.map (fun s -> s.Length) ["aaa";"e";"ttt"]
Je m'attendais à ce que le passage de l'argument permette d'inférer le type de s.
merci
Lorsque l'on accède à une méthode d'un objet, il faut que le type de l'objet soit connu "avant" (le système d'inférence a lieu de gauche à droite, c'est-à-dire selon l'ordre de lecture). Ce système peut sembler imparfait, mais il a l'avantage d'être simple à comprendre et assez prévisible. L'expression suivante devrait compiler :
let test = ["aaa";"e";"ttt"] |> List.map (fun s -> s.Length)
Une alternative ici (qui n'est pas possible dans le cas général) est d'utiliser à la place la fonction String.length (ou alors de mettre une annotation de type). Quand on n'utilise pas d'appel de méthode, l'inférence de type fonctionne beaucoup mieux, car il n'y a pas les difficultés liées à la surcharge.