Fonctions
Un article de Mangue.org, l'encyclopéde libre.
Les fonctions sont certainement la pierre angulaire de tous les programmes en Caml. Vous ne pourrez rien faire sans elles, c'est pourquoi ce cours est fondamental. Vous apprendrez à définir des fonctions simples puis des fonctions à plusieurs arguments.
Nous verrons également en fin de cours ce que sont les produits cartésiens et le filtrage, concepts fondamentaux qui nous permettront d'introduire la récursivité dans le cours suivant.
| Sommaire |
Définition d'une fonction
Il y a plusieurs façons en Caml de définir une fonction simple. Vous pouvez utiliser une notation très proche de celle que nous utilisons en mathématique ou introduire une notation plus formelle. A vous de choisir.
Dans l'exemple suivant nous souhaitons définir une fonction qui renvoie le successeur de son argument. Voici les deux manières envisageables :
# let suivant (x) = x + 1 ;; suivant : int -> int = <fun> # let suivant = fun x -> x + 1 ;; suivant : int -> int = <fun> # suivant (4) ;; - : int = 5 # suivant ;; - : int -> int = <fun>
Chacune de ces deux syntaxes a ses avantages et ses incovénients dont vous prendrez conscience au fur et à mesure.
Nous avons défini une nouvelle fonction. Comme tout ce que nous allons définir en Caml, celle-ci a une valeur (<fun> qu'il est impossible d'écrire autrement puisque celà dépend des valeurs passées en argument.) et un type (int -> int qui se lit "int vers int")
| Remarque |
De plus lorsqu'une fonction n'admet qu'un seul argument comme dans l'exemple ci-dessus il est possible d'utiliser le mot clé function à la place de fun.Le seul intérêt est justement de mettre en évidence le fait que la fonction n'admet qu'un seul paramètre. Etant donné que ceci n'est pas nécessaire, nous n'utiliserons pas ce mot clé dans nos programmes. |
Vous l'avez compris, une définition de fonction n'est pas très différente d'une définition de constante ou encore de chaîne de caractère (ce ne sont pas des constantes puisque leur valeur peut être modifié après leur définition). Nous définissions simplement une fonction d'un certain type à laquelle nous associons une valeur fonctionnelle.
Vous en avez déjà un exemple avec l'expression suivant(4) ... Pour appliquer une fonction sur un paramètre effectif il suffit de l'invoquer suivie de ce paramètre justement ;)
| Remarque |
| Les parenthèses ne sont pas requises autour des arguments, et celà que ce soit pour la définition ou pour l'application d'une fonction. Connaissant la paresse légendaire des programmeurs vous aurez certainement tendance à les ommettre. Cependant pour des raisons évidentes de lisibilité je vous conseille de les conserver à chaque fois. |
Définitions locales
Les fonctions sont en Caml des "objets" (par abus de langage) d'un certain type ayant une valeur fonctionnelle. Il est donc possible comme les autres "objets" de les définir localement. Admettons que nous ayons besoin de définir une fonction calculant le carré du carré de son argument entier (oui x4 en somme). ous choisissons de définir localement une fonction carré comme dans cet exemple :
# let puissance4 = let carré (x) = x * x in fun (x) -> carré ( carré (x)) ;; puissance4 : int -> int = <fun> # puissance4 (2) ;; - : int = 16
Evidemment il est possible de définir localement une constante afin d'assurer l'évaluation d'une expression fonctionnelle. Voir même de définir localement une fonction dans le seul but d'évaluer une expression comme dans cet exemple :
#let deg2rad = let Pi = 4.0 *. atan (1.0) in fun (x) -> x *. Pi /. 180.0 ;; deg2rad : float -> float = <fun> # deg2rad (90.0) ;; - : float = 1.57079632679 # let valeur_absolue (a) = if (a < 0) then (-a) else a in valeur_absolue (-5) * valeur_absolue (12) ;; - : int = 60
| Astuce |
| Dans cet exemple vous voyez aussi apparaître la première structure de choix simple. Voilà qui va nous permettre de construire des fonctions plus utiles :) En Caml le else est indispensable après le then, sauf si l'expression du then retourne une valeur de type unit. De plus les expressions placées respectivement après le then et le else doivent retourner un résultat de même type (int dans notre exemple)
|
Fonctions utilisant des constantes
| Question |
| Dans le corps d'une fonction il est possible d'utiliser la valeur d'une constante déjà définie. Mais que ce passera-t-il si par la suite nous redéfinissons une constante du même nom ? |
# let nombre = 4 ;; nombre : int = 4 # let ajoute_nombre (x) = x + nombre ;; ajoute_nombre : int -> int = <fun> # ajoute_nombre (5) ;; - : int = 9 # ajoute_nombre (-1) ;; - : int = 3 # let nombre = -2 ;; nombre : int = -2 # ajoute_nombre (2) ;; - : int = 6
Vous le constatez par vous même celà n'a absolument aucun effet sur la fonction. nombre étant justement une constante, lorsque vous définissez la fonction ajoute_nombre, c'est la valeur de la constante nombre qui est utilisée dans l'expression fonctionnelle.
Si vous redéfinissez une constante sous le même nom plus loin dans votre programme celà n'aura aucun effet sur la fonction elle même. Si vous avez besoin d'écrire des fonctions utilisant des paramètres extérieurs étudiez les fonctions à plusieurs arguments en contrebas ou mieux, le concept de références que nous verrons dans les cours suivants.
Les n-uplets
Il existe en Caml un constructeur très utile. Celui-ci permet de construire des n-uplets, c'est à dire des ensembles de valeurs n'appartenant pas nécessairement au même type.
Ces valeurs sont séparées par des virgules comme dans l'exemple suivant. Les n-uplets (on parle parfois de type produit cartésien) pourront être utilisés de manière judicieuse dans des fonctions comme nous le verrons par la suite.
Mais plutôt que de vous faire de longs discours voici nos premiers exemples ;)
# 1, 2 ;; - : int * int = 1, 2 # 1, true ;; - : int * bool = 1, true # 27.63, (), 25, true ;; - : float * unit * int * bool = 27.63, (), 25, true # int_of_char(`A`), `A`, (fun x -> x + 3) ;; - : int * char * (int -> int) = 65, `A`, <fun>
Il ne faut pourtant pas abuser des n-uplets puisque ceux-cis peuvent rapidement devenir très complexes et nuire à la lisibilité de vos codes sources. Prenez pour exemple le dernier qui fait intervenir une valeur fonctionnelle.
Il est même possible de construire des n-uplets de n-uplets, ce qui finira d'embrouiller ceux qui parmis vous commençaient à douter (rassurez vous c'est pas si effrayant mais si vous n'arrivez pas à comprendre foncez sur le forum sans plus attendre)
# let couple1 = 1, 2 and couple2 = 0.0, 2.5 and couple3 = (), int_of_char ;; couple1 : int * int = 1, 2 couple2 : float * float = 0.0, 2.5 couple3 : unit * (char -> int) = (), <fun> # couple1, couple2, couple3 ;; - : (int * int) * (float * float) * (unit * (char -> int)) = (1, 2), (0.0, 2.5), ((), <fun>)
Un des principaux intérêts des n-uplets est de permettre à une fonction de renvoyer plusieurs valeurs en même temps. En attendant de connaître une meilleure méthode (les fonctions à plusieurs arguments) vous pourriez très bien utiliser les n-uplets pour envoyer plusieurs valeurs en même temps à une fonction.
Il existe deux fonctions agissant sur les n-uplets, à savoir fst et snd qui renvoient respectivement la première et la deuxième valeur du dit n-uplet.
| Remarque |
| Il n'existe aucune fonction pour renvoyer la troisième valeur d'un n-uplet par exemple. Si vous en avez besoin vous devrez l'écrire vous même en utilisant le concept de filtrage. |
# fst (couple1) ;; - : int = 1 # snd (couple3) ;; snd ( couple1, couple2, couple3) ;; - : char -> int = <fun>
Les n-uplets vous offrent aussi une autre possibilité de définir plusieurs constantes en une seule instruction. Examinez l'exemple suivant, les deux requètes provoquent le même résultat ; la première est celle que nous connaissons déjà mais la seconde utilise les n-uplets.
Encore une fois les deux méthodes sont possibles, chacune apporte ses avantages et ses incovénients mais c'est à vous de choisir laquelle vous utiliserez.
# let a = 1 and b = 2 ;; a : int = 1 b : int = 2 # let (c,d) = (3,4) ;; c : int = 3 d : int = 4
Exercice : division entière
Un petit exercice ne fait pas de mal et ça vous aidera à assimiler plus facilement toutes ces nouvelles notions. Je vous demande donc d'écrire une fonction qui prends en argument au sein d'un n-uplet deux nombres entiers a et b et qui renvoie toujours sous forme de n-uplet deux valeurs. La première correspond au quotient de la division entière alors que la seconde correspond au reste.
Jouez le jeu ! Essayez de trouver la solution par vous même avant de lire ce qui suit ;)
#(* première version *) let division_entière (arguments) = let a = fst (arguments) and b = snd (arguments) in ( a/b, a mod b) ;; division_entière : int * int -> int * int = <fun> # division_entière (5,2) ;; - : int * int = 2, 1 #(* deuxième version *) let division_entière (a,b) = (a/b,a mod b) ;; division_entière : int * int -> int * int = <fun>
Ces deux fonctions produisent le même résultat. Vous pourriez vous demander au premier abord laquelle choisir mais la réponse est devant vos yeux ;)
La première version est certainement celle à laquelle vous avez pensé en premier, mais elle fait intervenir une définition locale de constante en son corps, ce qui nuit à la vitesse d'exécution ainsi qu'à la lisibilité. En revanche la seconde version est plus lisible puisqu'on voit du premier coup d'oeil à quoi correspond le n-uplet passé en argument ; cette fonction fait intervenir le concept de filtrage que nous allons voir sans plus tarder :)
Le filtrage
Derrière ce terme quelque peu obscur se cache un concept très simple. Les filtres sont utilisés tant pour décomposer des structures de données complexes comme nous l'avons fait dans l'exemple ci-dessus, que pour renvoyer des valeurs fonctionnelles différentes en fonction des paramètres.
Plusieurs constructeurs interviennent dans les filtres. Le plus important est certainement le | qui sert à séparer les différents filtres d'une fonction.
Examinez déjà cet exemple dans lequel la fonction compte_zéros prends en argument une paire d'entiers et renvoie le nombre de zéros présents dans cette paire (entre 0 et 2 donc!)
# let compte_zéros = fun | (0,0) -> 2 | (0,_) -> 1 | (_,0) -> 1 | _ -> 0 ;; compte_zéros : int * int -> int = <fun> # compte_zéros (1,0) + compte_zéros (5,-4) ;; - : int = 1
Le premier | (qu'on lit "pipe") n'est pas indispensable. De plus vous voyez apparaître des _ (lisez "underscore") dans les paires et même en tant que paramètre de la fonction. Nous n'avons pas encore eu l'occasion de rencontrer ce symbole qui peut remplacer n'importe quelle valeur de n'importe quel type.
Ainsi (0,_) représente une 0 suivi de n'importe quelle valeur (mais forcément de type int à cause des limitations de la fonction). De même _ sur la dernière ligne symbolise n'importe quelle valeur (ici un n-uplet de type int * int)
Lorsque vous appliquez une fonction définie par filtrage à un argument quelconque le Caml compare les motifs un à un en commençant par le premier. Dès qu'un motif correspond à ce que vous avez passé en argument, l'expression correspondante est évaluée.
Il en résulte qu'il convient de rester vigilant quant à l'ordre d'écriture de vos cas. Si dans compte_zéros nous avions commencé par le cas _ celui ci aurait été choisi à chaque fois puisqu'il aurait intercepté toutes les valeurs. Mais heureusement si vous commetez ce genre d'erreurs le Caml vous prévient :)
# let compte_zéros = fun | _ -> 0 | (0,_) -> 1 | (_,0) -> 1 | (0,0) -> 2 ;; Toplevel input: >| (0,_) -> 1 > ^^^ Warning: this matching case is unused. Toplevel input: >| (_,0) -> 1 > ^^^ Warning: this matching case is unused. Toplevel input: >| (0,0) -> 2 ;; > ^^^ Warning: this matching case is unused. compte_zéros : int * int -> int = <fun>
Les fonctions failwith et invalid_arg
Beaucoup des fonctions que vous écrirez ne pourront pas associer de résultat à certains paramètres. Reprenez par exemple la fonction division_entière écrite plus haut. Essayez de lui passer (1,0) en argument. Un message d'erreur apparaît.
Dans toutes les fonctions que vous écrirez à partir de maintenant il est souhaitable de prévoir les différentes erreurs possibles et de les traiter. Pour ça nous utilisons les fonctions failwith et invalid_arg.
Leur principal intérêt est d'afficher des messages d'erreurs plus facilement compréhensibles par l'usager de votre programme. Chacune de ces fonctions prends en argument une chaîne de caractère et renvoie un résultat de type 'a que nous apprendrons à exploiter plus tard.
Voici une version de la fonction division_entière indiquant à l'usager quelle est l'erreur :
# let division_entière = fun
| (_,0) -> invalid_arg "division_entière: Divison par 0, calcul impossible"
| (a,b) -> (a/b,a mod b) ;;
division_entière : int * int -> int * int = <fun>
# division_entière (5,2) ;;
- : int * int = 2, 1
# division_entière (-4,0) ;;
Uncaught exception: Invalid_argument
"division_entière: Divison par 0, calcul impossible"
Nous apprendrons justement dans un prochain cours comment intercepter ces erreurs et rendre les programmes plus robustes.
failwith et invalid_arg sont des fonctions similaires. Vous vous demandez certainement quand en choisir une plutôt que l'autre...
Il semble logique de réserver invalid_arg aux mauvais arguemtns justement ! Alors que vous pourrez utiliser failwith si une erreur se produit dans un calcul au sein de votre fonction.
Les fonctions à plusieurs arguments
Une fonction à plusieurs arguments est une fonction qui admet plusieurs paramètres. Je rappelle au passage qu'une fonction dont l'argument est un n-uplet n'a rien à voir avec une fonction à plusieurs arguments.
Plutôt que de nous perdre dans de longs discours, écrivons notre première fonction à plusieurs arguments :
# let multiplication a b = a * b ;; multiplication : int -> int -> int = <fun> # multiplication 3 4 ;; - : int = 12 # multiplication ;; - : int -> int -> int = <fun> # multiplication 5 ;; - : int -> int = <fun>
Lors de l'application de la fonction multiplication, c'est la fonction (multiplication 3) qui est appliquée à 4.
En fait vous pouvez voir les fonctions à plusieurs arguments comme des fonctions renvoyant des valeurs fonctionnelles. C'est exacteemtn ce qui se produit lorsqu'un ne fournit qu'un des deux arguments requis par multiplication. Celà nous renvoie une fonction de type int -> int qui assurera la multiplication de ses arguments par 5.
Un exemple complet est plus utile. Prenons le cas de la fonction logarithme en mathématiques. Toutes les fonctions logarithmiques que vous connaissez peut être sont construites sur le logarithme népérien (log en Caml).
La plus connue des fonctions utilisant directement le logarithme népérien est sans aucun doute le logarithme décimal défini comme ceci : log10 (x) = log (x) / log (10)
On peut construire tous les logarithmes possibles de cette manière bien que leur intérêt soit discutable. Pourtant il est fréquent d'utiliser les logarithmes en base 2 et en base 10.
Voici ce que nous devons faire en Caml :
# let Log n x = log(x) /. (log(float_of_int(n))) ;; Log : int -> float -> float = <fun> # (Log 10) 10000.0 (* on applique la fonction (Log 10) sur 10000.0 *) ;; - : float = 4.0 # (Log 2) ;; - : float -> float = <fun> # Log 2 256.0 ;; - : float = 8.0 # let log10 = Log 10 and log2 = Log 2 ;; log10 : float -> float = <fun> log2 : float -> float = <fun> # log10 0.001 ;; - : float = -3.0
Application partielle des fonctions à plusieurs arguments
Vous l'avez déjà vu dans les exemples précédents, si on ne fournit pas tous les éléments requis pour une fonction nous faisons ce que nous appellons une application partielle. C'est à dire que nous obtenons comme résultat une valeur fonctionnelle.
Cette valeur peut être utilisée justement pour définir une nouvelle fonction ou tout simplement pour l'appliquer à une valeur différente.
Cette fonctionnalité est très utilisée dans les programmes. Celà permet de faire des prototypes de fonctions génériques que l'on "spécialise" par la suite, la fonction Log en est un bon exemple.
| Réponse |
| Comme toujours foncez sur le forum si vous avez la moindre question :) |
Filtrage sur des fonctions à plusieurs arguments
Evidemment il est possible d'utiliser la puyissance du filtrage sur des fonctions à plusieurs arguments. La méthode est exactement la même que sur des fonctions à un seul argument. Il est moins facile de trouver un exemple concret avec de telles fonctions mais nous en verrons certainement dans les cours suivants.
Que ce soit pour les fonctions à un ou plusieurs arguments, le filtrage nous rendra de plus gros services lorsque nous commencerons à écrire des fonctions récursives dans le cours suivant.

