Aller au menu - Aller au contenu

Icône TP : Le type option

Avatar
Mise à jour : 18/11/2011
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-SA
973 visites depuis 7 jours, dont 47 sur ce chapitre classé 130/786
Nous allons étudier un type somme particulier, qui est bien connu des utilisateurs de langages fonctionnels, mais qui peinent à percer dans les langages plus industriels comme C# ou Java (bien que l'on y arrive progressivement). Pourtant, il est très pratique, et je pense que vous vous en rendrez compte si vous-même vous connaissez des langages non-fonctionnels.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire

Pourquoi le type option

Le type option représente la possibilité, pour une fonction, de ne pas avoir de résultat. En d'autres termes, il sert à décrire des fonctions partielles, ou encore à signaler un échec (par exemple lorsque l'on cherche l'indice d'un élément x dans une liste, et que cette liste ne contient pas x).

Un autre exemple bien connu des écoliers : la division par zéro. Si vous écrivez une application de calcul, pour que celle-si soit stable, vous êtes obligé de faire un test sur les nombres entrés par l'utilisateur lorsque celui-ci tente de faire une division — nous avons vu que OCaml ne tolère pas la division par zéro.

Mais le type de la fonction de division est (/) : int -> int -> int, c'est à dire « donne-moi deux int et je te renvoie un int ». Le type ne nous dit absolument pas que cette opération peut échouer si le deuxième argument est 0. Pourtant, si on s'imagine les types OCaml comme des ensembles (au sens mathématique), et les fonctions OCaml comme des fonctions mathématiques, on n'a pas vraiment envie de dire que (/) est vraiment une opération de ℤ×ℤ ->.

La plupart des langages utilisent alors une valeur particulière, null, qui est (quasiment) de tous les types à la fois. En quelque sorte, ils font « grossir » les types usuels, pour rajouter une nouvelle valeur dénotant l'absence de résultat. OCaml est beaucoup trop strict pour nous autoriser à faire une telle chose (et de toute façon cette approche pose certains problèmes). Mais les outils du précédent chapitre nous donnent une solution élégante !

Définition et utilisation

Option sur des entiers



Nous voulons définir un type qui représente ou bien l'absence d'information, ou bien exactement une information. Commençons doucement, supposons que l'information en question est toujours entière. Le fait d'avoir deux possibilités exclusives nous conduit à l'utilisation d'un type somme :

Code : OCaml
1
2
3
type int_option = 
 | Rien 
 | Un of int


Normalement, rien ne vous surprend ici (sinon, je vous conseille de retourner lire le chapitre des types somme !), et vous reconnaissez deux constructeurs — l'un d'arité nulle (il ne prend aucun argument), l'autre prenant un entier.

Une fois ce type défini, vous savez qu'il est possible de l'examiner avec un simple matchwith. On peut par exemple faire une fonction qui teste si son argument est Rien ou pas :

Code : OCaml
1
2
3
let est_rien x = match x with
  | Rien -> true
  | Un _ -> false


Un type option polymorphe



Mais pourquoi se limiter à un type option défini sur les entiers seulement ? Au chapitre précédent, nous avons montré comment définir un type de listes polymorphes, qui permettait de faire, avec les mêmes constructeurs, des listes d'entiers, de flottants, ou d'autres choses encore.

Exercice : En vous inspirant de ce qui a été fait dans le chapitre précédent, définissez un type option polymorphe. Recodez ensuite la fonction est_rien, et assurez-vous qu'elle a bien le type 'a option -> bool.

Cependant, il est en réalité inutile de redéfinir systématiquement le type option. De même que celui des listes, il est prédéfini en OCaml, avec les constructeurs None et Some (qui attend une valeur). Pour vous en convaincre, vous pouvez essayer de les utiliser directement dans la ligne de commande OCaml :

Code : Console
# None;;
- : 'a option = None
# Some 3;;
- : int option = Some 3
# Some (Some "salut");;
- : string option option = Some (Some "salut")


Mais il n'y a aucune magie là-dessous : option est défini exactement comme vous l'avez fait.

Remarquez que ce type polymorphe fait exactement ce que nous voulions : il « décore » le type α que nous lui donnons (int, string, etc.) en faisant la distinction entre les vraies valeurs (portées par Some) et l'absence de valeur (None).

Des fonctions utilisant option



Nous pouvons maintenant utiliser le type option dans des vrais programmes, et profiter de l'information supplémentaire qu'il donne. Quelles fonctions pourrions-nous bien définir ?

Recherche d'un élément



Par exemple, la recherche d'un élément dans une liste. Nous allons faire une fonction récursive, qui, étant donné un prédicat p : a -> bool (pour un type a donné) et une liste l, parcours l jusqu'à trouver un x qui satisfait p (et renvoie alors Some x), ou jusqu'à la fin de la liste (et là on renvoie None, car rien n'a été trouvé). Rien de bien difficile, si ?

Code : OCaml
1
2
3
4
5
6
7
let rec find p l = match l with
    | [] -> None
    | y::ys -> 
      if p y then
        Some y 
      else 
        find p ys


Exemple d'utilisation :

Code : Console
# let pair x = (x mod 2) = 0;;  (* Vrai si x est pair *)
val pair : int -> bool = <fun>
# find pair [15; 3; 4; 55];;
- : int option = Some 4
# find pair [15; 3; 55];;
- : int option = None


Cela paraît assez intuitif, à condition d'être habitué à la présence du Some. On ne peut pourtant pas s'en passer : pourquoi ne peut-on pas renvoyer directement y, quand on l'a trouvé ?

Deux exemples bien connus



Nous connaissons déjà des exemples de fonctions partielles, qui renvoient des erreurs lorsque nous les appliquons aux mauvais arguments. Outre la division, déjà citée en introduction, mentionnons par exemple List.hd (ou List.tl), qui échoue(nt) sur la liste vide.

Nous pouvons traduire cet « échec » par un None. Par exemple, dans le cas des listes :
Code : OCaml
1
2
3
let hd l = match l with 
    | [] -> None 
    | x::xs -> Some x


Exercice : Recodez tl. Pouvez-vous prédire le type de son résultat sans le demander à OCaml ? Recodez également la division d'un entier par un autre, avec une fonction de type int -> int -> int option

Des opérations supplémentaires

Nous l'avons vu, le type option nous force à décorer nos types avec de nouveaux constructeurs — qui peuvent demander l'écriture de code supplémentaire (des matchwith), ce qui est un peu rébarbatif.

Nous allons nous munir d'opérations supplémentaires qui rendent plus simple la manipulation de ces données.

Transformer un type option



On peut voir le type option comme une boîte contenant nos données, et portant des informations supplémentaires. La boîte nous dit s'il est pertinent ou non d'essayer d'extraire ces données, mais elle ne change pas les données elles-mêmes. Si ces données sont de type t1, alors nous manipulons des boîtes de type t1 option.

Une opération naturelle



Cependant, nos fonctions habituelles (de type t1 -> t2) ne peuvent pas s'appliquer directement à t1 option. Nous voudrions donc définir une opération générale qui, à partir d'une fonction f : t1 -> t2, permet d'obtenir une fonction de type t1 option -> t2 option (nous voulons que les données restent dans une boîte).

Remplacez un moment le type option par list : cela revient exactement à coder la fonction List.map (qui, elle, agit sur les listes) ! Dans le cas général, t1 et t2 sont inconnus. Notre fonction devrait donc avoir le type ('a -> 'b) -> ('a option -> 'b option), ce qui est équivalent à ('a -> 'b) -> 'a option -> 'b option (une fonction à un argument renvoyant une fonction à un argument, c'est pareil qu'une fonction à deux arguments, selon le principe de curryfication).

Exercice : Complétez le code suivant, et assurez-vous d'obtenir le bon type :

Code : OCaml
1
2
3
let map f x = match x with
    | None ->| Some y ->


Une petite réflexion sur les types



Puisque l'écriture de programmes correctement typés est l'une de nos préoccupations principales (pour notre plus grand plaisir !), penchons-nous une nouvelle fois sur le rapport entre le type de la fonction map précédente, et la façon dont nous l'avons définie. C'est un peu théorique — n'hésitez pas à laisser de côté cette partie pour l'instant, et à y revenir plus tard.

Lorsque vous avez complété le code de l'exercice précédent, vous n'avez pas eu beaucoup d'options (haha) différentes pour compléter les deux branches du matchwith. Notamment, au constructeur None, vous étiez obligé de faire correspondre le même constructeur, sans opération superflue.

Pourquoi ? Parce que toute fonction ou valeur particulière que vous auriez voulu utiliser aurait modifié le type de retour de notre fonction map. En gros, il est impossible de transformer un None en Some truc pour un truc particulier, car ce truc aurait forcément un type déterminé, et on aurait alors changé le type de map.

Ici encore, le type de notre opération nous donne plus d'informations que ce à quoi l'on s'attendait. Parce qu'elle est de type ('a -> 'b) -> 'a option -> 'b option, on sait qu'elle est suffisamment générale pour ne pas agir sur les données elles-mêmes, mais seulement sur leur emballage. En outre, on sait qu'un None ne peut être envoyé que sur un None — parce qu'il est impossible de générer une valeur qui soit compatible avec tous les types, c'est-à-dire de type 'b elle-même (pour tout 'b).

Composition de fonctions partielles



Nous allons maintenant nous donner un moyen de séquencer plusieurs opérations partielles, c'est à dire de les calculer les unes après les autres de façon à préserver un certain sens : si nous comprenons le type option comme le type des calculs pouvant générer une erreur, alors il faudra veiller à propager cette erreur.

Injection



Une opération simple, quand nous avons une donnée x d'un certain type, est l'injection de cette donnée dans un type option. Après tout, il suffit de la passer au constructeur Some. En général, les fonctions de ce genre sont appelées return. Si vous connaissez C ou Java, ces fonctions sont sans rapport avec l'instruction return des langages de programmation impératifs.

Posons dès à présent la fonction de type 'a -> 'a option suivante :
Code : OCaml
1
let return x = Some x


De même, on peut définir la valeur suivante, qui pourra être modifiée par la suite :
Code : OCaml
1
let fail = None


Opérateur de composition



Nous voulons définir un opérateur de composition entre nos différents calculs. Nous supposons que ceux-ci peuvent soit réussir, et retourner un résultat, soit échouer. Naturellement, un certain calcul peut aussi profiter du résultat de son ou ses prédécesseurs. Notre opérateur va donc récupérer un résultat (placé dans un type option), et l'envoyer sur une fonction réalisant un calcul à partir de ce résultat — pour renvoyer le résultat du deuxième calcul.

Ajoutons une toute petite contrainte : nous voulons qu'il soit facile d'utiliser la fonction return avec cet opérateur. D'ailleurs, return dénote elle aussi un calcul : celui qui renvoie sa donnée telle quelle.

L'usage est de noter >>= cet opérateur, et de l'appeler « bind » (« lier » en anglais). Son type est ( >>= ) : 'a option -> ('a -> 'b option) -> 'b option</code>. Cela ressemble assez à notre fonction <minicode type="ocaml">map, quand on y pense.

Exercice : Codez l'opérateur >>=. Pour définir un opérateur plutôt qu'une fonction, il faut entourer son nom de parenthèses. Ici, la définition commence par let (>>=) x f =.

On a par exemple

Code : Console
# Some 0 >>= fun x -> return (x + 1);;
- : int option = Some 1


ou encore

Code : Console
# Some 12 
  >>= fun x -> Some 5 
  >>= fun y -> return (x + y);;
- : int option = Some 17


Bien que ce code semble pour l'instant inutilement complexe, il est important que vous le compreniez.

Un exemple : un mini-interpréteur

Nous allons illustrer les fonctions précédentes au moyen d'un petit interpréteur pour un langage très simple, capable de faire des opérations arithmétiques sur des entiers. C'est pas la joie, c'est sûr, mais c'est un début ! En réalité, nous n'allons même pas décrire la façon d'analyser un code source pour le transformer en programme, mais partir d'une expression OCaml toute faite. Cette partie (appelée « analyse syntaxique ») ne nous intéresse pas ici, et demanderait trop de temps pour être détaillée. Si cela vous intéresse, vous pourrez toujours lire un tutoriel qui en parle.

Un interpréteur naïf



Type de données



Notre langage sera donc décrit directement en OCaml, à l'aide de types sommes, une fois de plus. Un tel type correspond à ce que l'on nomme « syntaxe abstraite » du langage que nous décrivons : c'est une façon de représenter les programmes du langage en étant « assez proche » de la syntaxe, sans pour autant s'embarrasser de phases d'analyse syntaxique. En gros, pour représenter l'opération 6 * (5 + 7), on manipulera une expression OCaml du style de Mul (Const 5, Add (Const 5, Const 7)).

Une expression arithmétique va alors être soit une constante (un entier), soit une opération binaire (addition, multiplication, soustraction ou division). Voici un (début de) type qui traduit ces différentes possibilités :

Code : OCaml
1
2
3
4
type expr = 
  | Const of int
  | Add of expr * expr
  |


Exercice : Complétez cette déclaration de type, avec les constructeurs Mul, Sub et Div.

Fonction d'évaluation



Une fois le type défini, il nous faut une fonction d'évaluation. Celle-ci devra recevoir une expr, et produire un int (du moins pour l'instant). Elle devra être récursive : pour évaluer une branche Add, nous devons calculer les deux sous-expressions portées par celle-ci !

Ici encore, voici le début :

Code : OCaml
1
2
3
4
5
6
7
let rec eval e = match e with 
  | Const n -> n
  | Add (e1, e2) -> 
    let n1 = eval e1 in
    let n2 = eval e2 in 
    n1 + n2
  |


Exercice : Vous savez ce que vous avez à faire. Cependant, inutile (pour l'instant) de compléter toutes les branches — contentez-vous de l'opération Div. Testez ensuite votre fonction sur des petits codes à base d'Add et de Div, et, en particulier, essayez de diviser par 0.

Une réécriture bénigne



Nous le voyons bien lorsque nous utilisons l'interpréteur OCaml (en ligne de commande, donc) pour écrire des bêtises : une erreur au niveau du langage interprété ne doit pas se traduire par un arrêt de l'interpréteur. Celui-ci, loin de perdre son sang-froid, doit signaler sa bourde à l'utilisateur, pour qu'il puisse la corriger.

Nous devons donc faire de même : ici, la seule erreur possible est (pour le moment) la division par zéro, et, comme nous ne savons rien faire d'autre, nous allons utiliser un type option. Plus précisément, notre fonction eval va désormais renvoyer un int option, indiquant son éventuel échec.

Pour comprendre quelles modifications apporter à notre code, nous allons supposer pendant un moment que return et (>>=) travaillent non plus sur des option, mais sur des types quelconques. Ils sont donc respectivement de types 'a -> 'a et 'a -> ('a -> 'b) -> 'b.

Exercice : Redéfinissez ces opérateurs.

Nous modifions maintenant notre fonction eval, juste pour comprendre où faire des modifications :

Code : OCaml
1
2
3
4
5
6
7
let rec eval e = match e with 
  | Const n -> return n
  | Add (e1, e2) -> 
    eval e1 
    >>= fun n1 -> eval e2
    >>= fun n2 -> return (n1 + n2)
  | etc.


Les noms choisis pour les deux opérateurs de la partie précédente semblent plutôt logiques : pour l'un, il s'agit « juste » de renvoyer un résultat, sans faire de calcul particulier. Pour l'autre, il s'agit de combiner un résultat et un calcul, à l'aide d'une fonction qui utilise ce résultat.

Exercice : Réécrivez le code de la branche Div pour qu'il ressemble à celui-ci. Rajoutez également le code nécessaire (à la déclaration du type expr ainsi que dans la fonction eval) pour mettre au carré une expression (par exemple, implémentez un constructeur Squ) — forcez-vous à utiliser (>>=) !

Une gestion des erreurs basiques



Si vous avez bien compris le fonctionnement de nos deux opérateurs, alors… repassons à la version qui utilise des types options. Pour l'instant, nous avons donc le code suivant :

Code : OCaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let return x = Some x

let (>>=) x f = 
  match x with 
  | None -> None 
  | Some y -> f y

type expr = 
  | Const of int
  | Add of expr * expr
  | Mul of expr * expr
  | Sub of expr * expr
  | Div of expr * expr

let rec eval e = match e with 
  | Const n -> return n
  | Add (e1, e2) -> 
    eval e1 
    >>= fun n1 -> eval e2
    >>= fun n2 -> return (n1 + n2)
  | Div (e1, e2) ->
    eval e1
    >>= fun n1 -> eval e2
    >>= fun n2 -> return (n1 + n2)
  | etc.


Nous voulons maintenant implémenter un système d'erreur basique pour notre langage. Il est clair que multiplier ou additionner deux nombres ne pose pas de problème… seule la division par 0 peut provoquer une erreur. Seulement, si on veut calculer par exemple Add (Div (3, 0), Const 5), il faut propager l'erreur correctement (c'est à dire la faire sortir du Add).

Or c'est précisément ce que fait l'opérateur (>>=) : propager les None. Par conséquent, nous avons déjà le bon code, sauf dans le cas du Div (a, b) : dans cette branche seulement, il faut évaluer et tester la valeur de b, car si elle est contient la valeur 0, nous échouons.

Voici une portion du code modifié :

Code : OCaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let fail = Nonelet rec eval e = match e with| Div (e1, e2) -> 
    eval e1
    >>= fun n1 -> eval e2
    >>= fun n2 -> 
      if n2 = 0 then fail else return (n1 / n2)
  | etc.


Pour plus de lisibilité, nous définissons également une valeur fail, qui deviendra une fonction par la suite (cf. exercices). Vous pouvez maintenant tester votre code : quand nous faisons un calcul sans erreur, tout va bien, mais quand une division par 0 apparaît, nous récupérons None pour toute l'expression.

Et map alors ?



Notre exemple illustre plutôt mal l'intérêt de la fonction map que nous avons définie précédemment. Pour combler cette lacune, un petit exercice :

Exercice : Rajoutez la possibilité (à votre type expr et dans eval) de calculer l'opposé d'un nombre (par exemple Neg (Const 3) sera évalué en Some -3). Trouvez comment utiliser map et une fonction (|>) : 'a -> ('a -> 'b) -> 'b.

Rappelez-vous que nous avons défini map directement avec un match. L'exercice précédent devrait normalement vous sembler redondant : en définissant (|>) : 'a -> ('a -> 'b) -> 'b, on fait apparaître une autre façon de combiner nos fonctions, qui est plus générale et ne dépend pas du type option. Nous aurions clairement pu y arriver avec (>>=) et return.

Exercice : Montrez que map peut être écrite grâce à ces deux fonctions, et n'a pas besoin d'être définie avec un match explicite.

Le mot de la fin

Des améliorations pour notre mini-langage



Des variables



On peut rajouter beaucoup de constructions à notre mini-langage. Notamment, on peut rajouter un constructeur Var of string à notre type expr pour travailler avec des variables.

Il faut alors modifier eval pour qu'elle travaille avec un environnement, c'est-à-dire une liste de couples (string, int). Quand le code évalué fait appel à une variable, il faut aller chercher la valeur de celle-ci dans la liste. Si elle n'y figure pas, c'est bien sûr une erreur. Par exemple,

Code : OCaml
1
2
3
4
# eval [("x", 3)] (Neg (Add (Const 2, Var "x")));;
- : int option = Some (-5)
# eval [("x", 3)] (Neg (Add (Const 2, Var "zozor")));;
- : int option = None


Exercice :
  • Commencez par programmer une fonction valeur x l qui va chercher la valeur correspondant à la chaîne x dans la liste de couples l. Vous pouvez réutiliser la fonction find définie précédemment, ou réécrire une fonction explicitement récursive.
  • Adaptez ensuite eval pour qu'elle sache traiter les variables. Grâce à (>>=), trois lignes suffisent !


Des instructions



On peut définir un autre type, instr, qui représente différentes sortes d'instructions pour notre mini-langage. Parmi celles-ci, certaines sont des affectations (on modifie ou crée une variable en lui affectant une valeur, qui est le résultat d'une expression), d'autres sont des affichages d'expressions (faisant éventuellement intervenir des variables).

Je propose par exemple

Code : OCaml
1
2
3
4
5
type instr =  
  | Print of expr
  | Affect of string * expr

type prog = instr list


Exercices (niveau avancé) :
  • Définissez une fonction exec : (string * int) list -> instr -> (string * int) list option, qui, à partir d'un environnement, exécute une instruction (passée en argument). Celle-ci modifie éventuellement l'environnement (définition d'une nouvelle variable ou modification de la valeur d'une autre), et il faut donc renvoyer la nouvelle liste de couples. Remarquez que cette instruction peut également conduire à une erreur, et que l'on travaille donc encore une fois dans le type option.
    • Vous aurez probablement besoin d'une fonction val insert : 'a -> 'b -> ('a * 'b) list -> ('a * 'b) list.
    • Pour afficher un entier n et renvoyer l'expression e1, utilisez le code print_int n; print_newline (); e1. Nous reviendrons prochainement sur les instructions, j'anticipe un peu pour l'instant !
  • Définissez ensuite une fonction run : instr list -> (string * int) list option. Si cela vous semble encore trop facile, remarquez que celle-ci peut s'exprimer à l'aide de la fonction List.fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a.
  • Vous pouvez naturellement encore enrichir notre mini-langage !


Les monades


Ce chapitre vous a probablement semblé long et difficile. Cependant, il contient des idées assez importantes : on a construit une méthode pour séparer, dans notre code, le traitement des erreurs d'un côté, et la propagation du résultat de l'autre. Plus précisément, nous avons des outils qui nous permettent de combiner des calculs, et de propager soit des résultats utiles, soit des informations « annexes » (ici, des erreurs).

Ces constructions sont récurrentes en programmation fonctionnelle. Elles correspondent un peu à ce qu'on appellerait un design pattern dans les langages orientés objet. On les appelle des monades. C'est un concept initialement mathématique, qui a été appliqué à l'informatique (initialement par le chercheur Eugenio Moggi) pour étudier les effets de bord dans les langages de programmation.

L'idée est que certains calculs, dans un programme, sont « purs », dans le sens où ils s'évaluent toujours de la même façon. C'est le cas par exemple d'une expression arithmétique, d'un nombre, etc. Cependant, certains autres calculs sont « impurs » : ils dépendent fortement du contexte, par exemple d'une valeur entrée par l'utilisateur. Pour comprendre ces effets de bord, Moggi a remarqué que beaucoup d'entre eux pouvaient être dotés d'une structure de monade, c'est-à-dire de fonctions (mathématiques) qui jouent le rôle de nos return et (>>=).

Un autre chercheur, nommé Philip Wadler, a ensuite proposé d'intégrer les monades directement dans les langages de programmation. Elles ne servent alors plus à étudier ces langages, mais deviennent partie intégrante des programmes, et permettent de structurer le code. Son article initial, Monads for functional programming, introduit (dans un pseudo-langage fonctionnel) un grand nombre d'exemples d'utilisation — notamment notre partie précédente — qui vont de la gestion des erreurs à celle des états mémoire, en passant par l'analyse syntaxique.

Les exceptions



Enfin, signalons que le type option, bien que prédéfini en OCaml, reste peu utilisé par la bibliothèque standard. En réalité, OCaml utilise plutôt des exceptions, qui sont un mécanisme de gestion des erreurs plus puissant a priori, mais qui correspond à la partie impérative du langage — nous reviendrons donc dessus dans les prochains chapitres.

Cependant, nous découvrirons que tout le travail que nous avons fait sur le type option n'est pas inutile : premièrement, grâce aux monades, nous avons schématisé la propagation des erreurs dans un programme. Les exceptions OCaml se comprennent exactement de la même façon, sauf qu'elles demandent des constructions particulières au niveau du langage lui-même, car ce ne sont pas des valeurs comme les autres (alors que nos options si). Certaines bibliothèques alternatives pour OCaml, telles Batteries, disposent d'un grand nombre de fonctions prédéfinies qui utilisent le type option.

Quoi qu'il en soit, pour de petits bouts de code, le type option s'avère pratique : il permet, grâce à des combinateurs comme (>>=), d'écrire du code relativement dense et lisible (il suffit de garder en tête que ce genre de combinateur propage les erreurs en priorité).

Des améliorations du type option



Si vous connaissez la valeur null du C (ou autres), remarquez que nos types options sont plus fiables : premièrement, parce qu'ils nous forcent à faire attention à eux, à cause du typage statique d'OCaml (le compilateur rejettera un code qui manipule un int option comme un int par exemple). En outre, ils supportent plusieurs niveaux d'imbrication ! Comment traduire find ((=) None) [Some 3; Some 2; None; Some 5] en C, par exemple ?

Enfin, il ne faut pas hésiter à redéfinir le type option pour lui ajouter plusieurs constructeurs. Par exemple, dans notre mini-langage, nous avons deux erreurs possibles : la division par zéro d'une part, et le fait d'utiliser des variables indéfinies d'autre part. Nous pourrions distinguer deux valeurs, None1 et None2, et réécrire (>>=) en conséquence (pour qu'il propage automatiquement ces deux valeurs) : cela suffirait pour distinguer, à l'arrivée, les deux sortes d'erreur possibles.

Nous pourrions aussi définir le type 'a either = Normal of 'a | Erreur of string, ou ce genre de chose. Ou encore changer le type (et (>>=) et return) pour qu'ils transportent des informations supplémentaires, comme le nombre d'instructions déjà exécutées par exemple.
Nous disposons maintenant d'une façon élégante de représenter des erreurs, et des fonctions partielles (c'est-à-dire définie sur un sous-ensemble de leur type). Bien qu'il existe des mécanismes plus puissants intégrés au langage, le nôtre a l'avantage d'être modulable, et très composable. Nous avons également découvert le concept de « monades », très important en programmation fonctionnelle !
Chapitre précédent Sommaire

Partager

Il n'y a pas encore de commentaire pour ce tuto.