Dans les langages fonctionnels, les fonctions sont des objets de « premier ordre ». Cela signifie qu'une fonction est un type de base, au même titre que les entiers ou les caractères. En pratique, on remarque en particulier que, comme les autres types, les fonctions :

  • peuvent être nommées ;
  • peuvent être anonymes ;
  • peuvent être passées en argument à une fonction (on appelle cela une fonction d'ordre supérieur) ;
  • peuvent être renvoyées par une fonction.

Les fonctions sont réellement des types de base et, tout comme on a les opérateurs +, -, *, etc. sur les nombres, il existe un certain nombre d'opérateurs pour combiner des fonctions et en créer des nouvelles.

Définition de fonctions

Pour définir une fonction, on utilise la même syntaxe que pour les autres types. Il suffit juste d'ajouter les arguments après le nom de la fonction.

let <ident> <arg> = <expression>

Par exemple :

> let sqr x = x * x;;
val sqr : int -> int

Remarquez le type de sqr : "int -> int". Cela signifie qu'il prend en argument un entier et renvoie un entier. Encore une fois : il n'y a généralement pas besoin de préciser le type, le compilateur le déduit tout seul. En l'absence de contexte, le compilateur choisit le type int pour les opérateurs numériques, notamment pour la compatibilité avec OCaml. Le contexte utilisé est large : si on utilise sqr sur des nombres flottants 10 lignes plus loin, c'est le type float qui sera utilisé. Toutefois, en mode interactif, le contexte disponible est très restreint : si vous souhaitez définir la fonction sqr sur les flottants, vous pouvez aussi ajouter une information de type :

> let sqrf (x: float) = x * x;;
val sqrf : float -> float

Le type de retour est alors déduit en conséquence. Si vous voulez faire une fonction sqr générique, qui accepte aussi bien des entiers que des flottants, il faudra attendre. J'en parlerai plus tard dans le cours, puisque cela fait appel à des fonctionnalités avancées[1].

Pour appeler une fonction, il suffit de mettre ses arguments à la suite (il ne faut pas mettre de parenthèses ou de virgules) :

> sqr 4;;
val it : int = 16

Le seul but des parenthèses est de fixer la priorité :

> sqr (5 + 3);;
val it : int = 64
> sqr 5 + 3;;
val it : int = 28
> sqr (-3)
val it : int = 9
> sqr -3
val it : int = 9

Pour le dernier cas, les parenthèses ne sont pas obligatoires, car le compilateur regarde l'espacement utilisé. Les fonctions à plusieurs arguments sont utilisées de la même façon :

> let distance x y =
    if x < y then
      y - x
    else
      x - y;;
val distance : int -> int -> int

Encore une fois, le type est clair : la fonction prend deux entiers en argument et renvoie un entier.

> distance 4 7;;
val it : int = 3
> distance (sqr 2) (sqr 4);;
val it : int = 12

Fonctions locales

De la même façon que l'on peut déclarer localement un entier, on peut déclarer localement une fonction, comme dans l'exemple qui suit :

> let x =
    let abs x =
      if x < 0 then
        -x
      else
        x
    abs (-4) * abs 5;;
val x : int
 
> x;;
val it : int = 20

Ici, une valeur simple (x) a été définie. Pour calculer sa valeur, on a défini localement une fonction. Cette fonction n'est pas globale et est inaccessible dès la fin de la déclaration de x.

Dans les beaucoup de langages, on peut seulement définir des variables locales au sein d'une fonction ; ici, nous venons de définir une fonction locale au sein d'une valeur simple.

Un autre exemple :

> [|0 .. 2 .. 10|].[ let f x = x + 1 in f 3];;
val it : int = 8

En pratique, ce code-là n'est pas toujours très conseillé pour des raisons de clarté. Je voulais surtout attirer l'attention sur le fait que l'on peut vraiment déclarer ce que l'on veut, à l'endroit que l'on souhaite. L'intérêt est de pouvoir limiter la portée des déclarations et de ne pas polluer l'espace de noms global.

De la même façon que l'on considère souvent qu'une variable globale est dangereuse dans certains langages, on peut aimer limiter au maximum les déclarations globales. Cela peut favoriser la relecture (une fonction qui n'est utilisée qu'une ou deux fois pourra être déclarée près du code). De plus, plus la portée est restreinte, plus on peut se permettre d'utiliser des identifiants courts. En pratique, cela peut réduire considérablement la longueur du code (sans en réduire la clarté).

Fonctions récursives

Par défaut, une déclaration de valeur n'est pas récursive. C'est-à-dire, dans la déclaration suivante :

  let x = x + 1

le x de l'expression fait référence à un ancien x (défini avant).

Si l'on veut définir une valeur récursivement, il faut utiliser le mot-clé rec :

> let rec fact x =
    if x < 2 then
      1
    else
      x * fact (x - 1)
val fact : int -> int
> fact 5;;
val it : int = 120

L'exemple de la fin (relisez le premier cours pour les opérations les listes) :

> let rec square_list n =
    let sqr x = x * x
    if n = 0 then []
    else (sqr n) :: square_list (n - 1);;
 
val square_list : int -> int list
 
> square_list 5;;
val it : int list = [25; 16; 9; 4; 1]

Notes

[1] De plus, une simplification de ce mécanisme est prévue dans F#. Je préfère attendre la nouvelle syntaxe avant d'en parler ici.