L04 - Fonctions anonymes, application partielle et composition

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

1. On Wednesday, March 4 2009, 21:00 by Stéphane

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!)

2. On Thursday, March 5 2009, 12:52 by Laurent

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.

3. On Tuesday, September 1 2009, 17:21 by larvor yann

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.

4. On Tuesday, September 1 2009, 21:24 by Laurent

Merci pour le retour, je viens de mettre à jour l'article.

5. On Wednesday, September 2 2009, 16:39 by yann l'arvor

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.

6. On Friday, November 20 2009, 10:50 by Stéphane

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

7. On Friday, November 20 2009, 16:30 by Laurent

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.