Le plus simple pour une première découverte : http://try.ocamlpro.com
Sinon vous pouvez directement lancer l'interpréteur `ocaml` dans une console.
Mieux encore, Lancez l'interpréteur OCaml sous l'éditeur `emacs` :
- ouvrez un nouveau fichier `exo.ml` (l'extension `.ml` est nécessaire),
- dans le menu `Tuareg`, dans le sous-menu `Interactive Mode`, choisir `Run OCaml Toplevel`
- confirmez le lancement de `ocaml` par un retour-chariot.
Chaque expression entrée dans la fenêtre de `exo.ml` peut être évaluée en se plaçant sur un caractère quelconque de l'expression, puis en utilisant le raccourci `ctrl-c,ctrl-e`, qui correspond à `Evaluate phrase` dans le sous-menu `Interactive Mode` du menu `Tuareg` d'emacs.
**Nota Bene** : dans emacs, on peut maintenant se passer de l'ancien terminateur `;;`. En contrepartie, *toutes* les phrases doivent commencer par `let`, même les tests. Au lieu de `1+2;;`, on écrira donc maintenant `let _ = 1+2`.
## Coeur fonctionnel d'OCaml
Présentation du lambda-calcul, avec ses trois constructions de base :
- variables
- abstraction de fonction : `λx.t` (correspondant à `fun x -> t` en OCaml)
- application de fonction : `t u` (idem en OCaml)
Règle de calcul : la β-réduction où `(λx.t) u` donne `t{x:=u}` (la définition de la substitution est laissée en exercice).
Exemple de propriété théorique de la réduction du lamda-calcul : la confluence.
Exemple de terme dont la réduction est infinie : `ΔΔ` valant `(λx.(x x))(λx.(x x))`.
Pas d'équivalent direct en OCaml pour des raisons de typage.
Typage simple du lambda-calcul :
- On type les variables en regardant dans un environnement de typage Γ
- Si `Γ+x:τ ⊢ t:σ` alors `Γ ⊢ (λx.t):(τ→σ)`
- Si `Γ ⊢ t:(τ→σ)` et `Γ ⊢ u:τ` alors `Γ ⊢ (t u) : σ`
Lien avec la logique minimale (gérant uniquement l'implication) : il suffit de ne garder que les types dans les règles précédentes.
C'est (le début de) l'isomorphisme de Curry-Howard.
Le typage d'OCaml est une évolution de ces règles de départ, avec en plus une notion de variables de type (`'a` dans la syntaxe OCaml).
Propriétés essentielles du lambda-calcul simplement typé :
- Préservation du typage lors de la réduction
- Normalisation forte : un terme bien typé ne peut avoir de réduction infinie.
Ceci combiné à la confluence signifie que la réduction d'un terme `t` finit toujours sur une unique forme normale `t₀` de `t`.
OCaml satisfait la première propriété (préservation du typage lors du calcul), ce qui est essentiel pour garantir toute une famille de plantage lors du calcul (cf. les "segfaults"). Par contre un système sans calcul infini est très restrictif, OCaml incorpore une notion de récursion générale, cf plus bas.
## Premières extensions OCaml
le `let` (global) et le `let ... in`.
Exemples:
```ocaml
letidentity=funx->x
(* ou de maniere equivalente: *)
letidentityx=x
(* projections, pouvant servir de pseudo-booléens *)
letproj1xy=x
letproj2xy=y
letpseudoifbxy=bxy
```
#### Exercice : Mécanisme de liaison en OCaml
Prévoir le résultat fourni par l'interpréteur OCaml après chacune des commandes suivantes :
```ocaml
letx=2
```
```ocaml
letx=3in
lety=x+1in
x+y
```
```ocaml
letx=3
andy=x+1in
x+y
```
Pourquoi la deuxième et la troisième commande ne fournissent-elles pas le même résultat ?
Réponses: le `let...in` de la deuxième phrase est une liaison *locale*, donnant 7 comme résultat de la phrase, tandis que après cette phrase `x` est de nouveau la définition globale valant 2. Quant à la phrase avec `let...and...in`, il s'agit d'une variante où les calculs sont fait en premiers, et les liaisons (locales) en second. Donc le `x+1` réfère au `x` global, avant qu'on ne crée un `x` et un `y` locaux valant tous les deux 3. Réponse finale 6.
Considérons maintenant les phrases suivantes :
```ocaml
letx=3
letfy=y+x
let_=f2
letx=0
let_=f2
```
Quels sont les résultats des appels de fonctions successifs `f 2` ?
Réponse: 5 dans les deux cas, en effet cacher la première définition globale de `x` par une nouvelle définition globale ne change rien pour la fonction `f` qui continue à utiliser le `x` visible au moment de la définition de `f`.
#### Exercice : Placement des parenthèses
Ajouter les parenthèses nécessaires pour que le code ci-dessous compile:
## Premiers types de données : Booléens et Entiers
OCaml fournit le type `bool` et ses constantes `true` et `false`.
On dispose d'une construction `if ... then ... else ...`.
Quelques opérations booléennes :
- la negation : `not`
- le "et" logique : `&&`
- le "ou" logique : `||`
Remarque : l'évaluation d'un `&&` ou d'un `||` suit des règles particulières (dites *paresseuses*), alors que le reste d'OCaml utilise normalement une évaluation *stricte* : calcul de tous les arguments d'une fonction avant de déclencher cet appel de fonction.
On peut obtenir des résultats booléens lors d'un test d'égalité `x = y` ou de différence `x <> y` ou d'ordre `x < y` ou `x <= y`, etc.
OCaml fournit un type `int` des entiers "machines" (donc à capacité bornée, attention aux débordements).
Il existe un type d'entiers non bornés si besoin.
Quelques opérations sur les entiers : addition `+`, multiplication `*`, division entière `/`, modulo `x mod y`.
#### Exercice : Fonctions booléennes
- Écrire une fonction `checktauto : (bool->bool)->bool` qui teste si une fonction booléenne à un argument répond toujours `true`.
- Même chose avec `checktauto2` et `checktauto3` pour des fonctions booléennes à 2 puis 3 arguments. On peut faire ça en énumerant à la main tous les cas, mais il y a évidemment plus malin, par exemple réutiliser `checktauto`.
- En utilisant des conditionnelles, écrivez une fonction `g` qui se comporte comme la fonction `f` ci-dessous. Vérifier ensuite à l'aide de `checktauto3` que `f` et `g` produisent bien toujours le même résultat.
Le `let rec` permet de réutiliser la fonction qu'on est en train d'écrire !
Attention à ne pas boucler ! Il faut donc prévoir un cas d'arrêt (p.ex. `x=0`) et faire des appels récursifs sur des valeurs décroissantes.
#### Exercice : écrire quelques fonctions récursives classiques
Factorielle
```ocaml
letrecfactorialn=
ifn=0then1
elsen*factorial(n-1)
```
Nombres de Fibonacci
```ocaml
letrecfibn=
ifn<=1then1
elsefib(n-1)+fib(n-2)
```
Attention, la fonction précédente va avoir un comportement exponentiel (pourquoi?).
Voici un exemple de version efficace.
```ocaml
letrecfib_loopnab=
ifn=0thena
elsefib_loop(n-1)b(a+b)
letfibn=fib_loopn11
```
Pgcd:
```ocaml
letrecpgcdab=
ifb=0thena
elsepgcdb(amodb)
```
Puissance
```ocaml
letrecpuissancean=
ifn=0then1
elsea*puissancea(n-1)
```
On peut également profiter de la structure binaire des nombres pour faire moins de multiplications:
```ocaml
letrecpuissancean=
ifn=0then1
elseifnmod2=0then
letb=puissancea(n/2)in
b*b
else
letb=puissancea(n/2)in
a*b*b
```
## Fonctions de première classse et application partielle
Des fonctions en argument ou en réponse ...
Des arguments qui manquent ...
Exemple : la composition de fonction
```ocaml
letcomposefg=funx->f(g(x))
```
Ou de manière équivalente:
```ocaml
letcomposefgx=f(g(x))
```
Bien noter (et comprendre!) le type inféré par OCaml: `('a->'b)->('c->'a)->'c->'a`
Autre exemple : sommation d'une fonction `f` entre les entiers `a` et `b`
```ocaml
letrecsumfab=
ifb<athen0
elsefa+sumf(a+1)b
```
Dernier exemple : un itérateur général de fonctions :
```ocaml
letreciterfnx=
ifn=0thenx
elseiterf(n-1)(fx)
```
Type inféré par OCaml : `('a->'a)->int->'a->'a`.
Exemple d'utilisation, pour retrouver la puissance:
```ocaml
letpuissancean=iter(funx->a*x)n1
```
## Type de données : les paires
Une paire regroupe deux éléments `(a,b)` qui ne sont pas forcément du même type.
Le type de cette paire s'écrit alors `τ * σ` si les types de `a` et `b` sont respectivement `τ` et `σ`.
Il y a également des triplets, quadruplets, n-uplets bâtis sur le même principe, par exemple `(1,2,true)` est un `int * int * bool`.
Pour utiliser une paire, soit on la projette (via les fonctions prédéfinies `fst` et `snd`),
soit on la déconstruit (via une syntaxe telle que `let (x,y) = ... in ...`.
Exemple : nombres de fibonacci successifs
```ocaml
letfibsn=
ifn=0then(1,1)
else
let(a,b)=fibs(n-1)in
(b,a+b)
letfibn=fst(fibsn)
```
Fonctions curryfiées ou décurrifiées.
Si une fonction OCaml a plusieurs arguments, l'usage est plutôt de mettre ces arguments les uns après les autres, ce qui donne un type de la forme `typ_arg1 -> typ_arg2 -> ... -> typ_res`. On parle de style *à la Curry*, ou *curryfié*.
Mais il est aussi possible de regrouper tous ces arguments dans un nuplet, ce qui donne un type de la forme `typ_arg1 * typ_arg2 * ... * typ_argn -> typ_res`. C'est le style *décurryfié*. Dans cette version, pas d'application partielle possible aisément. Par contre cela peut parfois être commode de traiter tout un groupe d'arguments comme un seul.
Passage d'une fonction binaire curryfiée en décurryfiée :
```ocaml
letuncurryf(x,y)=fxy
(* typage : ('a -> 'b -> 'c) -> 'a * 'b -> 'c *)
```
Passage d'une fonction binaire décurryfiée en curryfié :
```ocaml
letcurryfxy=f(x,y)
(* typage : ('a * 'b -> 'c) -> 'a -> 'b -> 'c *)
```
## Type de données : les listes
Une liste OCaml est un groupement ordonné d'éléments de même type, en nombre non prédéfini.
Exemple: `[1;2;3;4]` est une `int list`.
Les opérations élémentaires de construction de liste sont la liste vide `[]` et le prolongement
`x::l` d'une liste existante `l` par un nouvel élément `x` à gauche.
Notez que la syntaxe `[1;2;3;4]` n'est qu'un raccourci pour `1::(2::(3::(4::[])))`.
Pour analyser une liste, on peut utiliser des fonctions prédéfinies `List.hd` et `List.tl`, mais il est souvent plus élégant et à peine plus long d'utiliser une opération `match...with` (cf exemples ci-dessous).
Les listes (tout comme les paires auparavant, et les arbres ci-dessous) sont des structures *immutables* : une fois une liste constituée, on ne change plus son contenu. Par contre on peut former de nouvelles listes en réutilisant tout ou partie d'anciennes listes.
La tête de la liste (sa gauche) s'accède ou se ralonge en temps constant. Par contre le fond de la liste (sa droite) ne peut s'atteindre qu'en visitant toute la liste, donc en un temps linéaire (proportionnel à la taille de la liste).
Voici quelques exemples classiques
```ocaml
(* longueur d'une liste, déjà fourni par OCaml sous le nom List.length *)
letreclengthl=
matchlwith
|[]->0
|_::q->1+lengthl
(* concaténation de deux listes, déjà fourni par OCaml via la syntaxe @ *)
letrecappendl1l2=
matchl1with
|[]->l2
|x1::q1->x1::(appendq1l2)
(* concaténation retournant la première liste, cf. List.rev_append *)
letrecrev_appendl1l2=
matchl1with
|[]->l2
|x1::q1->rev_appendq1(x1::l2)
(* miroir d'une liste, cf List.rev *)
letrecrevl=rev_appendl[]
(* miroir d'une liste, version directe, mais en temps quadratique en la taille de l *)
letrecslow_revl=
matchlwith
|[]->[]
|x::q->(slow_revq)@[x]
(* filtre d'une liste selon un test booléen, cf List.filter *)
letrecfilterfl=
matchlwith
|[]->[]
|x::q->iffxthenx::filterfqelsefilterfq
(* action d'une fonction sur tous les éléments, cf List.map : ('a -> 'b) -> 'a list -> 'b list *)
letrecmapfl=
matchlwith
|[]->[]
|x::q->fx::mapfq
```
A régarder également dans la documentation : les itérateurs `fold_left` et `fold_right`.
Attention à l'usage sur de grosses listes (plus de 30000 éléments), certaines fonctions peuvent échouer avec l'erreur `Stack overflow`. Pour éviter cela, on peut utiliser le style *récursif terminal* (cf p.ex. `rev_append` ci-dessus) ... ou bien utiliser autre chose que des listes (arbres ou tableaux).
## Type de données : les arbres
Pas de type des arbres prédéfinis en OCaml, il y aurait trop de variantes selon les usages voulus. Heureusement, OCaml nous permet d'ajouter facilement nos propres *types algébriques* au système.
Par exemple, avec des éléments de type `'a` comme décoration à chaque noeud binaire:
Pour l'analyse, on utilise la construction `match...with` de manière similaire aux listes
(ou le raccourci `function`, qui correspond à un `fun` suivi d'un `match`).
```ocaml
letrectaillea=
matchawith
|Feuille->0
|Noeud(_,g,d)->1+tailleg+tailled
letrecprofondeur=function
|Feuille->0
|Noeud(_,g,d)->1+max(profondeurg)(profondeurd)
(* parcours infixe et conversion en liste *)
letrectolist=function
|Feuille->[]
|Noeud(x,g,d)->tolistg@[x]@tolistd
```
L'intérêt de ce genre d'arbre vient par exemple quand les données sont rangées par ordre croissant lors d'un parcours infixe, on parle alors d'arbre binaire de recherche (ABR).
```ocaml
(* fonction auxiliaire testant f sur tous les éléments d'un arbre *)
Note: cette version de la fonction `estabr` a une mauvaise complexité. Exercice : en écrire une version linéaire.
On peut alors faire de la recherche dichotomique pour savoir si un élément est dans l'arbre ou non:
```ocaml
letrecsearchxa=
matchawith
|Feuille->false
|Noeud(y,g,_)whenx<y->searchxg
|Noeud(y,_,d)whenx>y->searchxd
|Noeud_->true(* dernier cas possible : le noeud contient x *)
```
Si l'arbre est bien plat (c'est à dire complet ou presque), cette recherche est alors en temps
logarithmique en la taille de l'arbre. Il est possible de s'assurer que les arbres restent plats
même lorsqu'on les fait grossir, voir par exemple les arbres Rouges-Noirs ou les arbres AVL.
OCaml fournit par défaut une bibliothèque de fonctions ensemblistes rapides basées sur des arbres, voir le module `Set`.
## Types algébriques d'OCaml : faites vos propres types
Au delà des arbres, on peut continuer à concevoir des types algébriques correspondant à nos besoins. Par exemple, pour un petit langage de calcul formel:
Notez que le passage d'une chaine de caractère comme `"3x+sin(y)"` à la forme arborescente ci-dessus (de type `expr`) peut se faire automatiquement, mais ce n'est pas évident. Il s'agit d'une analyse lexicale et grammaticale, pour laquelle il existe des outils dédiés (cf `ocamllex` et `ocamlyacc`).
On peut alors écrire un simplificateur d'expression, essayant de se débarasser des constantes.
Ceci n'est qu'un début, à perfectionner.
```ocaml
letrecsimple=matchewith
|Binop(o,e1,e2)->
(matcho,simple1,simple2with
|Plus,Cstn1,Cstn2->Cst(n1+n2)
|Mult,Cstn1,Cstn2->Cst(n1*n2)
|Plus,Cst0,e2'->e2'
|Plus,e1',Cst0->e1'
|Mult,Cst0,_->Cst0
|Mult,_,Cst0->Cst0
|Mult,Cst1,e2'->e2'
|Mult,e1',Cst1->e1'
|o,e1',e2'->Binop(o,e1',e2'))
|Unop(u,e)->Unop(u,simple)(* TODO : simplifications liées aux fonctions unaires *)
|_->e(* pas de simplification pour une constante seule ou une variable seule *)
```
Enfin, voici un dérivateur formel. Le premier argument est le nom de la variable par laquel on dérive.
Le plus simple pour une première découverte : http://try.ocamlpro.com
...
...
@@ -89,10 +89,7 @@ and y = x + 1 in
x+y
```
Pourquoi la deuxième et la troisième commande ne fournissent-elles pas le même résultat ?
Réponses: le `let...in` de la deuxième phrase est une liaison *locale*, donnant 7 comme résultat de la phrase, tandis que après cette phrase `x` est de nouveau la définition globale valant 2. Quant à la phrase avec `let...and...in`, il s'agit d'une variante où les calculs sont fait en premiers, et les liaisons (locales) en second. Donc le `x+1` réfère au `x` global, avant qu'on ne crée un `x` et un `y` locaux valant tous les deux 3. Réponse finale 6.
Question: Pourquoi la deuxième et la troisième commande ne fournissent-elles pas le même résultat ?
Considérons maintenant les phrases suivantes :
...
...
@@ -104,22 +101,17 @@ let x = 0
let_=f2
```
Quels sont les résultats des appels de fonctions successifs `f 2` ?
Réponse: 5 dans les deux cas, en effet cacher la première définition globale de `x` par une nouvelle définition globale ne change rien pour la fonction `f` qui continue à utiliser le `x` visible au moment de la définition de `f`.
Question: Quels sont les résultats des appels de fonctions successifs `f 2` ?
#### Exercice : Placement des parenthèses
Ajouter les parenthèses nécessaires pour que le code ci-dessous compile:
Question: Ajouter les parenthèses nécessaires pour que le code ci-dessous compile: