IV. Valeurs structurées, polymorphisme et filtrage▲
Une valeur structurée est formée par assemblage de valeurs qui sont soit de même type, une liste par exemple, soit de types différents, un n-uplet par exemple.
Caml autorise la définition de fonctions polymorphes dont le type de l'argument est variable. Ce polymorphisme est particulièrement pratique pour construire des fonctions génériques sur des valeurs structurées : par exemple, le calcul de la longueur d'une liste qui ne nécessite pas la connaissance du type des éléments de cette liste ou bien l'extraction du i e élément d'un n-uplet, qui ne nécessite pas la connaissance du type de cet élément.
Une technique classique d'accès aux éléments d'une valeur structurée est le filtrage qui consiste à faire passer cette valeur au travers d'une structure de même forme, appelée filtre , dans laquelle chaque composante à accéder est désignée par un nom auquel elle sera liée. Par exemple, le filtrage de la paire (1, « un ») par le filtre ( c , m ) produira les deux liaisons : c = 1 et m = « un ».
Nous étudierons tout d'abord les n-uplets, ce qui nous permettra d'introduire le polymorphisme puis le filtrage et nous terminerons par l'étude des listes.
IV-A. N-uplets▲
Rappelons que le produit cartésien de n ensembles E 1 , …, E n , habituellement noté E 1 * … * E n , est l'ensemble des n-uplets ( e 1 , …, e n ) tels que, pour tout i de 1 à n , ei appartient à E i . Par exemple, le produit cartésien des ensembles {Jean} et {Pierre, Paul, Marie} est l'ensemble {(Jean, Pierre), (Jean, Paul), (Jean, Marie)}.
En Caml le produit cartésien de n types t 1 , …, tn est le type t 1 * … * tn (une expression de type) dont les éléments sont les n-uplets v 1 , …, vn tels que pour tout i de 1 à n , vi est une valeur de type t i . On a donc :
ext(t1 * … * tn) = {v1, …, vn | v1  ext(t1), …, vn  ext(tn)}
Par exemple « Jean », 35, true est un n-uplet de type string * int * bool. On notera que les parenthèses ne sont pas obligatoires pour encadrer un n-uplet, elles sont cependant conseillées pour améliorer la lisibilité.
Les n-uplets à deux éléments sont appelés des paires .
IV-A-1. Construction d'un n-uplet▲
Pour construire un n-uplet, Caml offre le constructeur multiinfixé « , ». Par exemple :
#("Jean" ^ "-Paul", 25 + 1, true);;
- : string * int * bool = "Jean-Paul", 26, true
La sémantique de ce constructeur est la suivante :
CONSTRUCTION D'UN N-UPLET. Si
Env
est un environnement et
e
1
, …,
en
sont des expressions telles que pour tout
i
de 1 à
n
,
type
(
e
i
,
Env
) =
ti
et
val
(
e
i
,
Env
) =
v
i
, alors
e
1
, …,
en
est une expression telle que :
type ( e 1 , …, e n , Env ) = t 1 * … * t n , val ( e 1 , …, e n , Env ) = v 1 , …, v n . |
Attention ! l'expression « e 1 , e 2 » construit une paire, l'expression « e 1 , e 2 , e 3 » construit un triplet, et l'expression « e 1 , e 2 , e 3 , …, e n » construit un n-uplet. Ce ne sont pas des valeurs de même type. Les constructeurs * et, ne sont donc pas associatifs comme le montre le programme suivant :
#(1, 2), 3;;
- : (int * int) * int = (1, 2), 3
#1, (2, 3);;
- : int * (int * int) = 1, (2, 3)
#1, 2, 3;;
- : int * int * int = 1, 2, 3
La première expression a pour valeur une paire formée d'une paire d'entiers et d'un entier, la seconde une paire formée d'un entier et d'une paire d'entiers et la troisième un triplet d'entiers.
IV-A-2. Égalité structurelle de n-uplets▲
En Caml l'égalité n'est définie que sur des valeurs de même type. Deux n-uplets de même type sont structurellement égaux si leurs éléments de même rang sont structurellement égaux.
IV-A-3. Accès aux éléments d'un n-uplet▲
L'accès aux éléments d'un n-uplet se fait normalement par filtrage comme nous le verrons au §III.C. Cependant, dans le cas des paires, deux fonctions prédéfinies sont disponibles fst et snd qui permettent d'accéder respectivement à la première et à la seconde composante. En voici un exemple :
#fst ("un", "deux");;
- : string = "un"
#snd ("un", "deux");;
- : string = "deux"
Ces deux fonctions étant polymorphes nous les étudierons au §IV.B consacré au polymorphisme.
IV-B. Polymorphisme▲
En Caml il existe un type particulier appelé paramètre de type (ou variable de type ) noté ' i où i est un identificateur (en général une lettre minuscule a, b, …). Si une valeur v est de type « paramètre de type » alors quelque soit le type t , v peut être considérée comme étant de type t .
Un type polymorphe (ou type paramétré ) est un type dont l'expression contient au moins un paramètre de type. Par exemple 'a et (int * 'a) sont des types polymorphes. Le premier a pour extension l'ensemble des valeurs Caml et le deuxième, l'ensemble des paires d'un entier et d'une valeur de type quelconque.
Une fonction polymorphe est une fonction dont le type de départ est polymorphe. Un exemple classique est la fonction id qui appliquée à une valeur retourne une valeur égale :
#let id = function x -> x;;
id : 'a -> 'a = <fun>
#id 1;;
- : int = 1
#id "un";;
- : string = "un"
Un autre exemple est l'opérateur prédéfini d'égalité :
#prefix =;;
- : 'a -> 'a -> bool = <fun>
On notera que les arguments doivent être du même type, comme le prouve à contrario le programme suivant :
#1 = true;;
> Toplevel input:
>1 = true;;
> ^^^^
> Expression of type bool
> cannot be used with type int
Les fonctions d'accès aux composantes d'une valeur structurée sont polymorphes, de façon à les rendre indépendantes des types de ces composantes. C'est le cas notamment des fonctions d'accès au premier et au deuxième élément d'une paire que nous avons présentées au §III.A.3 :
#fst;;
- : 'a * 'b -> 'a = <fun>
#snd;;
- : 'a * 'b -> 'b = <fun>
IV-C. Filtrage▲
Le filtrage est une technique très utilisée dans les langages de programmation fonctionnels ou logiques, car elle améliore énormément la lisibilité des programmes.
Un filtre visualise la structure d'une valeur et nomme certaines de ses composantes. Le filtrage est l'opération consistant à faire passer une valeur au travers d'un filtre. Si elle réussit, elle produit un environnement qui lie les noms (on dit aussi les variables) du filtre aux composantes correspondantes de la valeur.
Commençons par un exemple :
let (nom, _, age, ((ville, "France") as adresse)) = ("Dupont", "Jean", 12, ("Marseille", "France"));;
ville : string = "Marseille"
adresse : string * string = "Marseille", "France"
age : int = 12
nom : string = "Dupont"
La construction (nom, _, age, (ville, "France") as adresse) est un filtre. Le filtrage par ce filtre du quadruplet ("Dupont", "Jean", 12, ("Marseille", "France")) déclenche :
- le filtrage de "Dupont" par le filtre nom qui produit la liaison nom : string = "Dupont" ;
- le filtrage de "Jean" par le filtre _ qui réussit car il filtre n'importe quelle valeur ;
- le filtrage de 12 par le filtre age qui produit la liaison age : int = 12 ;
- le filtrage de la paire ("Marseille", "France") par le filtre (ville, "France") as adresse qui déclenche :
• le filtrage de "Marseille" par le filtre ville qui produit la liaison ville : string = "Marseille",
• le filtrage de "France" par le filtre "France" qui réussit,
- puis produit la liaison adresse : string * string = "Marseille", "France".
Soit F un filtre et v une valeur, nous noterons env_filtré ( F , v ) l'environnement produit en liant les variables de F aux composantes correspondantes de v . La fonction env_filtré définit la sémantique du filtrage.
Tout filtre a un type qui est celui des valeurs qu'il filtre.
Nous considérerons tout d'abord un sous-ensemble des filtres de Caml :
- le filtre _ filtre toute valeur. Si v est une valeur on a env_filtré (_, v ) = {} ;
- un identificateur filtre toute valeur. Si i est un identificateur et v une valeur de type t alors a env_filtré ( i , v ) = { i : t = v }. i devient le nom de la valeur v ;
- une valeur filtre toute valeur qui lui est égale. Si v est une valeur alors env_filtré ( v , v ) = {} ;
- si F1, …, Fn sont des filtres alors F1, …, Fn filtre tout n-uplet v1, …, vn tel que pour tout i de 1 à n, Fi filtre vi. On a : env_filtré(F1, …, Fn, v1, …, vn ) = env_filtré(F1, v1 ) … env_filtré(Fn, vn ) ;
- si F est un filtre et n est un identificateur, alors F as n est un filtre qui lie n à la valeur filtrée. Si v est une valeur de type t et F est un filtre de même type, alors on a : env_filtré ( F as n , v ) = env_filtré ( F , v ) { n : t = v } ;
- si F est un filtre alors ( F ) est un filtre équivalent à F .
Nous verrons par la suite, qu'à chaque type structuré est associé un filtre dont la syntaxe est identique à celle du constructeur des valeurs de ce type.
IV-C-1. Noms définis par filtrage▲
Les filtres peuvent être utilisés dans les clauses let des définitions globales ou locales. Les noms définis sont les identificateurs présents dans les filtres et les liaisons sont obtenues par filtrage.
Soit F 1 , …, Fn des filtres, une définition globale a la forme générale suivante :
let F1 = e1 and … and Fn = en ;;
Et une expression à définitions locales a la forme générale suivante :
let F1 = e1 and … and Fn = en in e;;
Par exemple :
#let (lon, lar, haut) = (15, 5, 20);;
haut : int = 20
lar : int = 5
lon : int = 15
#let (lon, lar, haut) = (15, 5, 20) in lon * lar * haut;;
- : int = 1500
On notera que ces nouvelles clauses let généralisent celles étudiées au §I.E, puisqu'un identificateur est un cas particulier de filtre. Les règles d'évaluation et de typage sont elles-mêmes généralisées de la façon suivante :
ENVIRONNEMENT AJOUTE PAR UNE DEFINITION NON RECURSIVE. Si Env est un environnement et pour tout i de 1 à n, Fi est un filtre et ei est une expression valide dans Env, alors : Env_def(let F1 = e1 and … and Fn = en, Env) = env_filtré(F1, val(e1, Env)) … env_filtré(Fn, val(en, Env)). |
TYPE ET VALEUR D'UNE EXPRESSION A DEFINITIONS LOCALES. Si
Env
est un environnement et pour tout
i
de 1 à
n
,
Fi
est un filtre et
ei
est une expression valide dans
Env
, et
e
est une expression valide dans
Env
Env_def
(let
F
1
=
e
1
and … and
F
n
=
e
n
,
Env
), alors :
type (let F 1 = e 1 and … and Fn = en in e , Env ) = type ( e , Env Env_def (let F 1 = e 1 and … and Fn = e n , Env )), val (let F 1 = e 1 and … and Fn = en in e , Env ) = val (e, Env Env_def (let F 1 = e 1 and … and Fn = e n , Env )). |
IV-C-2. Fonctions définies par filtrage▲
Le filtrage fournit un moyen élégant de définir une fonction : la définition par cas , qui généralise le mode de définition que nous avons présenté au chapitre 2. Si F 1 , …, Fn sont des filtres et e 1 , …, en sont des expressions, l'expression :
function F1 -> e1 | … | Fn -> en
définit une fonction qui appliquée à un argument x rend la valeur de l'expression ei associée au premier filtre Fi qui filtre x , en essayant successivement les filtres F 1 , …, Fn dans cet ordre. Pour des raisons de symétrie, on peut placer une | devant le premier cas. Par exemple :
#let nul_ou_non_nul =
function
| 0 -> "nul"
| _ -> "non nul";;
nul_ou_non_nul : int -> string = <fun>
#nul_ou_non_nul 0;;
- : string = "nul"
#nul_ou_non_nul 1;;
- : string = "non nul"
On remarquera que le mode de définition que nous avons présenté au chapitre 2 (function x -> e ) n'est qu'un cas particulier de définition par cas puisqu'un nom de variable est un filtre.
Lorsque la définition ne comporte qu'un seul cas on peut écrire :
let n F = e
au lieu de ;
let n = function F -> e
Par exemple :
#let moyenne (x, y) = (x +. y) /. 2.;;
moyenne : float * float -> float = <fun>
Les règles de typage et d'évaluation d'une définition de fonction énoncées au §II.A, sont généralisées de la façon suivante :
TYPE D'UNE DEFINITION DE FONCTION. Soit
Env
un environnement et function
F
->
e
| … |
Fn
->
en
une fonction à typer dans cet environnement. On désigne par
f
la fonction définie par cette expression, par
t
et
t
' les types de départ et d'arrivée de
f
, par
t
, …,
tn
les types de
F
, …,
Fn
et par
t
'
, …,
t
'
n
, les types de
e
, …,
e
n
. On pose les contraintes
t
=
type
(
F
,
Env
) = … =
type
(
F
n
,
Env
) et
t
' =
type
(
e
,
Env
) = … =
type
(
e
n
,
Env
). Une analyse du type de chaque sous-expression qui compose les expressions
e
1, …
en
, permet d'établir un ensemble de contraintes sur
t
et
t
'. Trois cas peuvent alors se présenter :
|
VALEUR D'UNE DEFINITION DE FONCTION. Si
Env
est un environnement et function
F
1
->
e
1
| … |
Fn
->
en
est une définition de fonction valide dans cet environnement alors :
val (function F 1 -> e 1 | … | Fn -> e n , Env ) = fermeture ( F 1 -> e 1 | … | Fn -> e n , Env ). |
VALEUR D'UNE APPLICATION DE FONCTION. Soit
Env
un environnement et soit
e
et
e
' deux expressions telles que
val
(
e
,
Env
) =
fermeture
(
F
1
->
e
1
| … |
Fn
->
e
n
,
Env
d
) et
val
(
e
',
Env
) =
a
. On recherche le premier des filtres
F
1
, …,
F
n
, parcourus dans cet ordre, qui filtre
a
. S'il en existe un, soit
i
son rang, alors :
val ( e e ', Env ) = val ( e i , Envd „v env_filtré ( F i , a )) sinon l'application est impossible. |
Une opération classique, dans la plupart des langages de programmation, est la sélection qui choisit l'action à réaliser en fonction de la valeur d'une expression (par exemple l'instruction « switch » de C). En Caml, cette opération se traduit par l'application d'une fonction définie par cas :
(function F1 -> e1 | … | Fn -> en) e
qui s'écrit aussi :
match e with F1 -> e1 | … | Fn -> en
La valeur calculée dépend de la valeur de e et les filtres F 1 , …, Fn jouent le rôle de sélecteur.
Par exemple la fonction suivante donne la traduction en anglais des 4 saisons :
#(match x with
| "printemps" -> "spring"
| "été" -> "summer"
| "automne" -> "autumn"
| "hiver" -> "winter"
| _ -> "Ce n'est pas une saison !") automne;;
- : string = "autumn"
La construction if…then…else que nous avons étudiée au §1.4 n'est pas une construction de base de Caml. Elle est un cas particulier d'application d'une fonction de type de départ bool. On a :
if e1 then e2 else e3
≡ (function true -> e2 | false -> e3) e1
≡ match e1 with true -> e2 | false -> e3
IV-C-3. Exhaustivité du filtrage▲
Lorsqu'une fonction est définie par filtrage, toutes les occurrences du type de départ de cette fonction doivent être prises en compte. Si l'on ne le fait pas, la définition sera acceptée mais le message suivant sera affiché :
> Warning: pattern matching is not exhaustive.
Par exemple :
#let un_deux_trois = function
| 1 -> "un"
| 2 -> "deux"
| 3 -> "trois";;
Toplevel input:
>....................function
> | 1 -> "un"
> | 2 -> "deux"
> | 3 -> "trois"..
Warning: this matching is not exhaustive.
un_deux_trois : int -> string = <fun>
On veut définir une fonction applicable à tout entier mais seuls les entiers 1, 2 et 3 ont été considérés. Il faut donc définir la valeur de cette fonction pour les autres entiers. Par exemple :
#let un_deux_trois = function
| 1 -> "un"
| 2 -> "deux"
| 3 -> "trois"
| _ -> "pas un, pas deux, pas trois";;
un_deux_trois : int -> string = <fun>
IV-D. Listes▲
Rappelons qu'une liste est une suite finie, éventuellement vide, d'éléments. Il est habituel de construire une nouvelle liste en ajoutant un élément e en tête d'une liste l : e est la tête de la nouvelle liste et l sa queue . Par exemple :
- cons (printemps, liste (été, automne, hiver)) = liste (printemps, été, automne, hiver) ;
- tête ( liste (printemps, été, automne, hiver)) = printemps ;
- queue ( liste (printemps, été, automne, hiver)) = liste (été, automne, hiver).
Inversement on parcourt et l'on traite les éléments d'une liste par une opération récursive qui consiste à accéder et à traiter la tête de la liste puis à relancer l'opération sur la queue de la liste jusqu'à atteindre une liste vide.
En Caml les listes sont homogènes, ce qui signifie qu'elles sont constituées de valeurs de même type. Le type t list a pour éléments les listes de 0, 1, 2, … valeurs de type t . On a donc :
ext(t list) = []  {[v1] | v1  ext(t)}  {[v1; v2] | v1  ext(t), v2  ext(t)}  …
Les éléments d'une liste sont encadrés par des crochets et séparés par un point-virgule. Le symbole [] désigne la liste vide. Quelque soit le type t , la liste vide appartient au type t list. La liste vide est donc une valeur polymorphe. Par exemple, les listes [] et [1; 3; 5; 7; 9] sont de type int list.
IV-D-1. Construction d'une liste▲
Le constructeur de liste est l'opérateur infixé et associatif à droite :: qui construit une liste à partir de sa tête et de sa queue (c'est l'opérateur cons de Lisp). Par exemple :
#"printemps" :: "été" :: "automne" :: "hiver" :: [];;
- : string list = ["printemps"; "été"; "automne"; "hiver"]
#("un", 1) :: ("deux", 1 + 1) :: ("trois", 2 + 1) :: [];;
- : (string * int) list = ["un", 1; "deux", 2; "trois", 3]
La sémantique du constructeur de liste est la suivante :
CONSTRUCTION D'UNE LISTE. Si
Env
est un environnement et si
e
1
et
e
2
sont deux expressions telles que
type
(
e
1
,
Env
) =
t
,
val
(
e
1
,
Env
) =
v
,
type
(
e
2
,
Env
) =
t
list et
val
(
e
2
,
Env
) =
l
, alors
e
1
::
e
2
est une expression telle que :
type ( e 1 :: e 2 , Env ) = t list, val ( e 1 :: e 2 , Env ) = [ v ] si l = [], [ v , v 1 , … , vn ] si l = [ v 1 , … , v n ] |
L'équivalence syntaxique suivante permet d'améliorer la lisibilité de la construction d'une liste de longueur connue :
[e1; … ; en]  e1 :: … :: en :: []
Par exemple :
#[("un", 0 + 1); ("deux", 1 + 1); ("trois", 2 + 1)];;
- : (string * int) list = ["un", 1; "deux", 2; "trois", 3]
IV-D-2. Égalité structurelle des listes▲
Deux listes vides sont structurellement égales. Deux listes non vides de même type, sont structurellement égales si leurs têtes et leurs queues sont structurellement égales.
IV-D-3. Tête, queue et concaténation▲
La tête et la queue d'une liste peuvent être extraites par les fonctions prédéfinies hd et tl ou bien par filtrage comme nous le verrons ci-dessous. La concaténation de deux listes est réalisée par l'opérateur prédéfini et infixé @. Par exemple :
#let les_saisons = ["printemps"; "été"; "automne"; "hiver"];;
les_saisons : string list = ["printemps"; "été"; "automne"; "hiver"]
#hd;; (* tête *)
- : 'a list -> 'a = <fun>
#hd les_saisons;;
- : string = "printemps"
#tl;; (* queue *)
- : 'a list -> 'a list = <fun>
#tl les_saisons;;
- : string list = ["été"; "automne"; "hiver"]
#prefix @;; (* concatenation *)
- : 'a list -> 'a list -> 'a list = <fun>
#[hd les_saisons] @ (tl les_saisons);;
- : string list = ["printemps"; "été"; "automne"; "hiver"]
IV-D-4. Filtrage des listes▲
Trois filtres sont disponibles pour les listes : le premier pour la liste vide, le second pour accéder à la tête et à la queue d'une liste et le troisième pour accéder aux éléments d'une liste de longueur connue :
- [] filtre []. On a env_filtré ([], []) = {}.
- F 1 :: F 2 filtre toute liste l dont la tête t est filtrée par F 1 et la queue q est filtrée par F 2 . On a env_filtré ( F 1 :: F 2 , l ) = env_filtré ( F 1 , t ) env_filtré ( F 2 , q ).
- [ F 1 ; …; F n ] filtre toute liste [ v 1 ; … ; v n ] telle que, pour tout i de 1 à n , Fi filtre v i . On a env_filtré ([ F 1 ; …; F n ], l ) = env_filtré ( F 1 , v 1 ) … env_filtré ( F n , v n ).
Par exemple :
#match ["Le"; "langage"; "Caml"] with
| [] -> "Liste vide !"
| m::_ -> "Le premier mot est " ^ m;;
- : string = "Le premier mot est Le"
#match les_saisons with
| [] -> "Y a plus de saisons !"
| [s1; s2; s3; s4] → "Le " ^ s1 ^ ", l'" ^ s2 ^ ", l'" ^ s3 ^ " et l'" ^ s4;;
- : string = "Le printemps, l'été, l'automne et l'hiver"
IV-D-5. Exemples de manipulations de listes▲
Les listes sont associées aux langages fonctionnels depuis la naissance de ceux-ci (le nom du premier des langages fonctionnels, Lisp, est un acronyme pour « List processing »). Un grand nombre d'opérations sur cette structure de données sont devenues des classiques. La plupart sont d'ailleurs prédéfinies en Caml (dans le module list de la bibliothèque de base ). Nous allons en étudier quelques unes afin de mettre en évidence la technique de traitement des listes qui est basée sur la récurrence structurelle :
PRINCIPE DE RECURRENCE STRUCTURELLE SUR LES LISTES. Si une propriété P est vraie pour [] et si dès que P est vraie pour l alors P est vraie pour x :: l , alors P est vraie pour toutes les listes.
Compter les éléments d'une liste
Pour compter les éléments d'une liste on utilise la propriété suivante : le nombre d'éléments d'une liste vide est égal à 0 et le nombre d'éléments d'une liste non vide est égal à 1 plus le nombre d'éléments de la queue de cette liste. Ce qui donne :
#let rec compter l =
match l with
| [] -> 0
| _ :: q -> 1 + (compter q);;
compter : 'a list -> int = <fun>
#compter les_saisons;;
- : int = 4
Appartenance d 'un élément à une liste
Pour vérifier qu'une valeur appartient à une liste on utilise la règle suivante : si cette liste est vide, cette valeur ne lui appartient pas, si elle n'est pas vide cette valeur lui appartient soit si elle est égale à la tête de cette liste, soit si elle appartient à la queue de cette liste. Ce qui donne :
#let rec membre v l =
match l with
| [] -> false
| t::q -> if t = v then true else membre v q;;
membre : 'a -> 'a list -> bool = <fun>
#membre "automne" les_saisons;;
- : bool = true
#membre "winter" les_saisons;;
- : bool = false
Select …from…where
Le langage SQL a popularisé l'opérateur « select…from…where », qui appliqué à un ensemble de tables : (i) en construit le produit cartésien, (ii) le restreint aux lignes qui vérifient une condition donnée, et enfin (iii) le projette sur certaines colonnes. Nous en donnons ici une version simplifiée, la fonction select_from_where qui appliquée à une fonction f , à une liste l et à un prédicat p extrait la liste formée par l'application de la fonction f à chaque élément de l qui vérifie le prédicat p .
Pour définir la fonction select_from_where nous utiliserons deux fonctions intermédiaires filter et map. La fonction filter élimine d'une liste les éléments qui ne vérifient pas un prédicat donné. Par exemple :
filter [1; 2; 3; 4; 5; 6; 7; 8; 9] est_pair = [2; 4; 6; 8]
Elle peut se définir de la façon suivante :
- le filtrage d'une liste vide produit une liste vide ;
- le filtrage d'une liste l de tête x par un prédicat p produit :
- la liste ayant pour tête x et pour queue le résultat du filtrage de la queue de l par p , si x vérifie p (on garde x ),
- le résultat du filtrage de la queue de l par p , si x ne vérifie pas p (on élimine x ).
La fonction map applique une fonction à chaque élément d'une liste. Par exemple :
map [1; 2; 3] est_pair = [false; true; false]
Elle peut se définir de la façon suivante :
- le « mapping » d'une liste vide produit une liste vide ;
- le « mapping » d'une liste l de tête x par une fonction f produit la liste ayant pour tête f x et pour queue le « mapping » de la queue de l par f .
En utilisant ces deux fonctions on obtient :
select_from_where f l p = map (filter l p) f.
D'où la définition complète de select_from_where en Caml :
let select_from_where f l p =
let rec
filter l p =
match l with
| [] -> []
| t::q -> if p t then t::(filter q p) else (filter q p)
and
map l f =
match l with
| [] -> []
| t::q -> (f t)::(map q f)
in
map (filter l p) f;;
select_from_where : ('a -> 'b) -> 'a list -> ('a -> bool) -> 'b list = <fun>
Considérons maintenant une liste d'enfants, où chaque enfant est représenté par son prénom et son âge :
#let les_enfants = [("Claire", 13); ("Alain", 8); ("Paul", 12); ("Marie", 5)];;
les_enfants : (string * int) list = ["Claire", 13; "Alain", 8; "Paul", 12; "Marie", 5]
La requête « Quels sont les prénoms des enfants de plus de 10 ans » peut s'exprimer de la façon suivante :
#let prénom = function (p, a) -> p
and plus_de_10_ans = function (p, a) -> a > 10
in
select_from_where prénom les_enfants plus_de_10_ans;;
- : string list = ["Claire"; "Paul"]
Remarquons enfin, que la fonction select_from_where aurait pu être définie directement de la façon suivante :
#let rec select_from_where f l p =
match l with
| [] -> []
| t::q -> if p t then
(f t)::(select_from_where f q p)
else
(select_from_where f q p);;
select_from_where : ('a -> 'b) -> 'a list -> ('a -> bool) -> 'b list = <fun>