Polymorphisme

Un article de Mangue.org, l'encyclopéde libre.

Nous allons maintenant aller plus loin en Caml en découvrant le polymorphisme... Concept très intéressant nous permettant par exemple de construire des fonctions applicables à plus types de données en même temps.


Sommaire

Présentation du polymorphisme

Dans toutes les fonctions que nous avons écrit jusqu'à présent le Caml était capable de déterminer de manière précise un type pour chaque expression. Examinez cet exemple dans lequel nous définissons les fonctions minimum et maximum :

# let minimum a b = if (a<b) then a else b
  and maximum a b = if (a>b) then a else b ;;
minimum : 'a -> 'a -> 'a = <fun>
maximum : 'a -> 'a -> 'a = <fun>
# minimum 3 4 ;;
- : int = 3
# maximum "test" "retest" ;;
- : string = "test"

Les fonctions qui sont typées avec un 'a (ou encore 'b, 'c, etc.) sont des fonctions polymorphes, c'est à dire des fonctions pour lesquelles le Caml n'a pas pu déterminer un type unique.
En effet la seule contraite sur les fonctions minimum et maximum est que les deux arguments soient de même type. Il peut s'agir de n'importe quel type tant que les opérateurs de comparaison ont un sens dessus (bool, int, char, float, etc.)

Vous avez déjà utilisé des fonctions polmorphes sans le savoir, souvenez vous de fst et snd

# fst ;;
- : 'a * 'b -> 'a = <fun>
# snd ;;
- : 'a * 'b -> 'b = <fun>

'a et 'b sont deux types différents (mais qui peuvent s'avérer être identique dans certains cas), il s'agit de types inconnus...
En effet nous avons vu que les fonctions fst et snd pouvaient être appliquées sur n'importe quelle paire de valeurs, peu importait le type, il pouvait s'agir de int * int ou encore de (bool -> string) * unit


Eviter le polymorphisme

Dans certains cas vous aurez besoin d'une fonction uniquement pour un type de données connu et vous ne souhaiterez pas qu'elle soit polymorphe puisque celà pourrait introduire des erreurs dans votre programme. Vous avez dans ce cas la possibilité d'expliciter le type d'uen expression. Ceci existe tant pour les constantes que pour les fonctions, mais voyons plutôt comment celà fonctionne :

# ("Bgw" : string) ;; (* attention les parenthèses sont obligatoires! *)
- : string = "Bgw"
# let ( entier : int) = 63 ;;
entier : int = 63
# let (minimum: int -> int -> int) = fun a b ->
  if (a < b) then a else b ;;
minimum : int -> int -> int = <fun>

Remarque
Certains d'entre vous préfèreront peut-être expliciter à chaque fois le type de leurs expressions. Ca permet en effet de se rapporcher encore plus des définitions mathématiques, pourtant je ne le ferrai pas dans ces cours puisque celà alourdirai (inutilement) le code.

La syntaxe est la même que lors des répliques du Caml, il vous faut indiquer l'identificateur auquel vous voulez associer une valeur ou directement la constante voulue suivie d'un : et du type tel que le Caml l'afficherait dans sa réplique justement.
Vous avez la possibilité d'ajouter des contraintes de types comme celle-ci dans tous vos programmes afin, par exemple, d'en faciliter la relecture ou tout simplement, comme ici, d'obtenir des fonctions polymorphes.

Dans toutes les fonctions que nous avons défini dans les cours précédents il était possible d'expliciter le type. Nous ne l'avons pas fait pour ne pas trop alourdir le code source. Les deux définitions suivantes sont équivalente, à vous de juger laquelle vous préférez :

# let valeur_absolue_de_la_différence a b = if (a<b) then b-a else a-b ;;
valeur_absolue_de_la_différence : int -> int -> int = <fun>
# let (valeur_absolue_de_la_différence: int -> int -> int) = fun a b ->
  if (a<b) then b-a else a-b ;;
valeur_absolue_de_la_différence : int -> int -> int = <fun>
# valeur_absolue_de_la_différence 7 4 ;;
- : int = 3
# valeur_absolue_de_la_différence 1 (-5) ;;
- : int = 6
# valeur_absolue_de_la_différence (-9) 13 ;;
- : int = 22


Exercices

Ce cours peut paraître simple, mais il est pourtant essentiel. Même si le polymorphisme a rapidement été exposé (je persiste à ne pas vous noyer sous de longs discours), ce n'est pas quelque chose qu'il faut survoler.
C'est un concept indispensable ! Nous l'utiliserons fréquemment dans les programmes des cours suivants. C'est pourquoi je vous propose ici quelques exercices moins tutorés que d'habitude à résoudre. Dans chaque énoncé je décris précisément ce que votre fonction doit faire et je suggère uen solution possible (mais pas toujours la seule!), si vous avez des problèmes faites vous aider sur le forum. Une solution pour ces exercices est accessible depuis le sommaire des cours.

Nous souhaitons écrire une fonction (polymorphe) qui prends en argument deux valeurs au sein d'un n-uplet et qui nous renvoie la paire inversée. Les exemples sont là pour vous montrer le comportement précis de la fonction, à vous de l'écrire !

# échange_valeurs (1,3) ;;
- : int * int = 3, 1
# échange_valeurs ( fst, ()) ;;
- : unit * ('_a * '_b -> '_a) = (), <fun>
# échange_valeurs (`t`,(false,0)) ;;
- : (bool * int) * char = (false, 0), `t`
# échange_valeurs ;;
- : 'a * 'b -> 'b * 'a = <fun>

Remarque
'_a et '_b sont à considérer comme 'a et 'b ... Si Caml les écrit comme celà c'est parce qu'il s'agit du type du résultat d'une fonction polymorphe. Pour vous il n'y a aucune différence.

On vous demande maintenant d'écrire une fonction prenant en argument un couple de couple de valeurs et renvoyant un résultat du même type mais avec les valeurs inversées. Les exemples suivants vous permettront certainement mieux de comprendre ce que fait la fonction ;)
Indication : il peut être judicieux d'utiliser la fonction échange_valeurs

# échange_double_de_valeurs ( (1,2), (3,4)) ;;
- : (int * int) * (int * int) = (4, 3), (2, 1)
# échange_double_de_valeurs ( (false,0), (true,1)) ;;
- : (int * bool) * (int * bool) = (1, true), (0, false)
# échange_double_de_valeurs ( (nth_char,()), (`r`,échange_valeurs)) ;;
- : (('_a * '_b -> '_b * '_a) * char) * (unit * (string -> int -> char)) =
 (<fun>, `r`), ((), <fun>)
# échange_double_de_valeurs ;;
- : ('a * 'b) * ('c * 'd) -> ('d * 'c) * ('b * 'a) = <fun>

Nous désirons maintenant écrire une fonction permettant de composer d'autres fonctions. Exactement comme en mathématiques lorsque vous faites sin ( cos (x) ) par exemple.

# compose float_of_int int_of_char `A` ;;
(* float_of_int ( int_of_char ( `A`)) *)
- : float = 65.0
# compose (fun x -> x + 1) ( fun x -> x * x) (6) ;;
- : int = 37
#let Pi = 4.0 *. atan(1.0) in
  compose sin cos (Pi /. 2.0) ;;
(* sin ( cos ( Pi / 2)) *)
- : float = 6.12303176911e-017
# compose ;;
- : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>

Bon courage pour la résolution de ces exercices. Encore une fois je le répète, n'hésitez surtout pas à poser vos questions sur le forum !


Choses surprenantes avec le polymorphisme

Avec le polymorphisme il est possible d'écrire des fonctions qui n'ont absolument aucun intérêt ;)
Prenez par exemple la fonction qui suit, à n'importe quel argument elle associera la même valeur, un résultat de type unit en plus !

# let dummy _ = () ;;
dummy : 'a -> unit = <fun>
# dummy fst ;;
- : unit = ()
# dummy "Ce cours est génial !" ;;
- : unit = ()
# dummy 27.63 ;;
- : unit = ()
# dummy (81,(),dummy,int_of_char `t`) ;;
- : unit = ()

Cette fonction dummy peut pourtant avoir un intérêt non négligeable dans des programmes complexes. Imaginons que nous ayions à récupérer le résultat d'un traitement sans en tenir compte, on ignore totalement la valeur de l'expression renvoyée lors de la dernière évaluation. Dans ce cas la fonction dummy (que nous aurions pu appeller poubelle) permet de se débarasser de cette valeur.
Très utile au sein d'une séquence. D'ailleurs la fonction quit qui met fin au système interactif Caml est elle-même de type 'a -> unit c'es à dire qu'elle ne tient pas compte du dernier résultat et effectue son traitement.

Bien que son intérêt soit limité il est également possible d'écrire sur le même modèle uen fonction identité renvoyant exactement son argument : let identité x = x


Curification et décurification

La curification est un procédé qui permet de transformer une fonction à n paramètres au sein d'un n-uplet en une fonction à n arguments.
Vous l'avez deviné la décurification est le procédé inverse permettant de transformer une fonction à n en une fonction admettant en argument un n-uplet constitué des différentes valeurs.

# let cury f x y = f (x,y)
  and uncury f (x,y) = f x y ;;
cury : ('a * 'b -> 'c) -> 'a -> 'b -> 'c = <fun>
uncury : ('a -> 'b -> 'c) -> 'a * 'b -> 'c = <fun>
# map ;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>
# let app = uncury map ;;
app : ('_a -> '_b) * '_a list -> '_b list = <fun>
# let map_ = cury app ;;
map : 'a -> ('_b -> '_c) -> '_b list -> '_c list = <fun>

Ce sont les fonctions pour des fonctions à 2 paramètres uniquement, il faudrait les généraliser pour les appliquer dans un cas général mais ce n'est pas l'objet de ce cours.
L'intérêt de ces fonctions est limité biensûr mais il n'est pas impossible que vous les utilisiez dans un programme. Et de toutes façons ça ne peut pas vous faire de mal de voir de nouvelles fonctions :)

Outils personels