Il est temps de s'émanciper de la calculette, et d'introduire les constructions nécessaires pour écrire des programmes sophistiqués. Étonnament, il suffit de seulement deux nouvelles constructions : la création et l'application de fonctions.
Le principe même de la programmation est de décrire des processus complexes, en les décomposant en processus simples. On construit donc à partir de briques élémentaires, les littéraux et les opérateurs que nous avons vu à la section précédente. Ces briques sont combinées pour former des composantes, les composantes sont elle-mêmes combinées pour former des composantes plus complexes, jusqu'à obtenir le programme désiré.
Le principal travail du programmeur consiste à identifier les composantes qu'il va devoir fournir aux étapes intermédiaires. Il lui faut déterminer leur rôle, leur responsabililité dans le programme. Cette identification nécessite bien sûr un identifiant, c'est-à-dire un nom, qui permette de se référer au composant, de l'utiliser dans d'autres composants. Le nommage d'un composant est extrêmement important :
Ce deuxième point est exactement le rôle des variables en mathématiques. Si je veux calculer la quantité de patates produites par un champ rectangulaire de largeur 20 m et de longueur 30m, et que le rendement est de 4kg.m2, peu importe la valeur précise de ces nombres, je sais que la quantité produite est décrite par les formules :
$$\textrm{aire du champ} = \textrm{largeur} \times \textrm{longueur} $$
$$\textrm{quantité de patates} = \textrm{aire du champ} \times \textrm{rendement par m$^2$}$$
Nous pouvons donc abstraire les données du problème par des variables, au sens mathématiques, c'est à dire des noms permettant de masquer et manipuler des valeurs. Listons quelques avantages de procéder ainsi :
si je veux calculer combien d'argent je vais toucher en vendant les patates, je peux maintenant utiliser directement l'abstraction quantité de patates avec une autre formule simple :
$$\textrm{recette} = \textrm{quantité de patates} \times \textrm{prix au kg}$$
ce qui m'évite de devoir écrire une formule complexe faisant intervenir la largeur de mon champ et le prix des patates dans la même expression.
Voilà pourquoi la tâche principale du programmeur est de nommer : nommer les quantités, nommer les calculs. On peut créer un nom de deux façons : soit le nouveau nom dépend d'autres expressions nommés, soit le nouveau nom ne dépend pas d'autres noms. Dans le premier cas, cela s'appelle une fonction : une fonction décrit la dépendance d'une valeur avec une ou plusieurs autres valeurs. Le deuxième cas peut être considérer comme une variante du premier cas, avec une valeur qui dépend de 0 autre valeur. Donc ce que nous avons besoin, c'est simplement de pouvoir construire des fonctions, et de leur donner un nom.
Étant maintenant compris qu'une variable et une abstraction sont la même chose, c'est à dire une valeur (qui peut être connue ou indeterminée) abstraite sous la forme d'un identifiant, nous employerons les deux termes indistinctement. Remarquons aussi que notre notion de variable n'est pas la même que dans les programmes impératifs : on appellera assignable les soi-disant variables des programmes impératifs. Un assignable est un conteneur d'une valeur, la valeur contenue pouvant être remplacée par une autre. Ce n'est donc pas une abstraction, puisqu'une abstraction est directement une valeur. Cela n'a pas de sens de dire qu'une valeur abstraite est modifiée; une valeur n'est pas modifiable, elle existe et c'est tout. 42 ne peut pas changer de valeur, ça ne veut rien dire.
La syntaxe pour définir une abstraction est :
L'identifiant est le nom (informellement) donné à l'abstraction. L'expression définit la valeur ainsi abstraite. let se lit soit en français, comme lors de l'introduction d'une variable dans une preuve en mathématiques. La phrase complète se lit "Soit tel identifiant défini comme la valeur de telle expression" .
L'identifiant se construit par une séquence de lettres, minuscules ou majuscules, de chiffres, et les caractères _ et ' (apostrophe). Tous les autres caractères sont interdits. La longueur de la séquence est arbitraire. La première lettre doit impérativement être une minuscule. Il est d'usage en OCaml de séparer les mots d'un identifiant par _, plutôt qu'utiliser comme en Java des majuscules pour marquer les premières lettres de chaque mot.
Nous avons discuté de l'importance de nommer en programmation. Cela implique que les noms doivent être bien choisis : chaque identifiant doit décrire le rôle de l'expression abstraite. Il faut bien sûr établir un compromis entre la longueur de l'identifiant (que nous souhaitons court) et la qualité de sa description (que nous voulons précise). C'est souvent assez difficile de trouver des bons noms, et il est normal de passer beaucoup de temps juste pour cela lorsqu'on programme.
Lorsqu'on échoue à trouver un nom approprié, c'est généralement le symptôme d'un programme mal conçu. L'abstraction peut ne pas être la plus adaptée pour le problème. Elle peut ne pas être assez précise, ou au contraire l'être trop. Le programmeur peut ne pas être convaincu du rôle de la variable, ce qui veut dire qu'il devrait faire une pause, prendre du recul sur son travail et re-situer le contexte de ce qu'il essaie de programmer. Tant qu'il n'y a pas adéquation entre la valeur abstraite et l'identifiant donné à l'abstraction, la création de l'abstraction ne peut être considérée comme aboutie.
Une façon de bien cerner quel identifiant utiliser est d'imaginer des petites modifications de l'abstraction : si je change légèrement le rôle et la valeur de la variable, comment pourrais-je refléter ce changement dans le nom ? En quoi le nom que j'ai choisi me permet de distinguer ma variable de sa variante légèrement modifiée ? Si je ne peux pas faire cette distinction, comment le lecteur de mon programme pourra-t-il la faire ?
Le choix du nom doit tenir compte du contexte dans lequel l'abstraction va être employée. Les programmes que nous écrivons doivent être faciles à lire, ils doivent donc avoir une syntaxe et une grammaire aussi proche de l'anglais que possible (puisque le langage est en anglais, on utilisera intégralement cette langue, y compris pour les noms de variables). Ainsi :
En cas de doute, on se place toujours dans le contexte d'utilisation de l'abstraction, et on essaie de rendre cette utilisation la plus lisible et évidente possible. Le critère ultime est que le programme doit être facile à comprendre.
La création de variables tel que nous venons de la voir permet de définir des abstractions qui sont ensuite utilisables dans toute la suite du module dans lequel cette création apparait. On parlera alors de variables module-locales.
Il est possible de définir des variables localement à une expression, que nous appelerons variables expression-locales (ou simplement locale). Par exemple, lors de la définition de la recette obtenue par la vente des patates, on peut vouloir définir le prix par kilogramme uniquement pour l'expression de la recette :
Ici, l'abstraction price_per_kg est définie localement pour l'expression qui suit in, c'est-à-dire harvested_quantity * price_per_kg, comme une abstraction de la valeur $1.0$. L'abstraction rice_per_kg ne sera donc pas définie dans la suite du programme.
La syntaxe générale pour définir une variable expression-locale est
Ceci représente une expression. On obtient sa valeur, en remplaçant dans l'expression suivant le in, tous les identifiants correspondant à cette abstraction par la valeur de l'abstraction. Dans l'exemple, income a donc la même valeur que harvested_quantity *. 1.0.
Quelle est la règle de typage associée ?
Par exemple,
Si harvested_quantity n'était pas de type float, l'expression complète ne serait pas bien typé.
Lorsqu'on essaie d'utiliser un identifiant qui n'a pas été défini, le compilateur produit un message d'erreur et interrompt la compilation. Le message d'erreur est unbound variable ce qui veut dire variable non-liée. En effet, la création d'une variable lie un identifiant avec une valeur. Si l'identifiant n'est pas créé, il n'est pas lié à une valeur, c'est ce que rappelle le message d'erreur. On parle donc de liaison pour la création d'une paire identifiant-valeur, c'est-à-dire une abstraction.
À l'inverse, un identifiant peut avoir été défini plusieurs fois, et liés à plusieurs valeurs (qui ne sont même pas nécessairement du même type). Il est tout à fait autorisé d'utiliser plusieurs fois le même identifiant. Dans ce cas, toutes les liaisons continuent d'exister, autrement dit, différentes variables ayant le même identifiant coexistent.
Se pose alors la question : comment le compilateur décide quelle est la bonne liaison, pour chaque occurence de l'identifiant ? Certains langages permettent la surcharge : les compilateurs utilisent le type pour savoir de quelle liaison il est question. Cependant, OCaml infère les types, il ne les connait pas à l'avance et ne peut donc pas deviner grâce au type quelle est la liaison adéquate. De plus la surcharge tend à rendre les programmes plus complexes à lire, puisque trouver la définition d'un identifiant devient non-trivial. OCaml utilise donc une règle simple : définir un identifiant déjà existant masque l'ancienne liaison, tant que la nouvelle liaison existe. Une liaison existe seulement dans l'expression où elle est explicitement définie dans le cas d'une variable expression-locale, et jusqu'à la fin du module dans le cas d'une variable module-locale. C'est donc la dernière liaison valide qui compte.
Un simple exemple suffit à comprendre sans rentrer dans le formalisme nécessaire.
Bien sûr, il faut éviter de donner le même nom à plusieurs variables si cela peut prêter à confusion. On peut utiliser le même nom par exemple pour les arguments de différentes fonctions, si ces arguments ont toujours le même rôle, car cela rend le programme plus cohérent, et que les arguments sont clairement propres à chaque fonction. Par contre un manque d'imagination n'est pas une excuse valide pour réutiliser le même nom plusieurs fois, rappelons une fois encore que le nommage des variables est critique et constitue une partie essentielle du travail de programmation.