Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Programmer en pensant fonctionnel (F#)–Partie 2

[new:30/08/2014]Dans l’article précédent j’ai introduit les raisons et les difficultés de penser autrement son code. Il est maintenant temps de mettre les mains dans le cambouis et de comprendre par l’étude de F# ce que signifie “penser fonctionnel”.

Les fonctions mathématiques au cœur du fonctionnel

Le mécanisme même de la programmation fonctionnelle est issu des mathématiques et notamment du concept de fonction. Le nom même de programmation fonctionnelle est une évocation directe des fonctions mathématiques et de leurs caractéristiques que les langages ainsi conçus tentent d’émuler au mieux pour les mettre au service du développement.

Prenons une fonction mathématique qui ajoute une constante à un nombre :

Add(z) = z+1

 

Le sens d’une telle définition est évident : il existe une fonction appelée “Add” qui pour “z” lui fait correspondre la valeur z+1.

Toute une terminologie spécifique tourne autour de ce type de représentation symbolique. Par exemple l’ensemble des valeurs admises pour les paramètres d’entrée est appelé le domaine. Dans le cas de notre exemple le domaine pourrait être l’ensemble des réels ou autres. Supposons ici qu’il s’agit uniquement d’entiers.

L’ensemble des valeurs de sortie produites par la fonction est appelée son étendue (range en anglais). Dans le cas de l’exemple il s’agit aussi d’entiers.

De fait la fonction sert à dresser une carte des chemins du domaine vers l’étendue (range). C’est une opération de mapping où l’on fait correspondre les éléments d’un ensemble à ceux d’un autre. En terme mathématique c’est une application.

image

La définition d’une telle fonction en F# s’écrira :

let Add x = x + 1

 

L’écriture est simple et directe. Parfaitement claire et lisible même pour qui ne connait pas les règles syntaxiques de F#. L’opérateur “let” pose une définition, ici celle de l’opération “Add” qui intervient sur un paramètre nommé “x” en lui faisant correspondre la valeur “x + 1”.

Comme vous le constatez il n’y a pas d’indication de type ! Horreur, malheur c’est encore un machin de type script à la noix non typé !  … Calmez-vous, je ne vous ferai jamais un coup pareil. Non, F# est un vrai langage typé, fortement même. Seulement il utilise l’inférence de type pour attribuer au domaine et à l’étendue un type précis qui ne varie pas dans le temps. Si la fonction Add est appelée avec un entier elle ne traitera plus que des entiers. Si elle est appelée avec un float elle ne traitera plus que des float.

Alors il vrai que ce typage qui dépend de la première utilisation me gêne personnellement car il suffit qu’un gugusse insère une ligne de code quelque part pour que le type de la fonction change sans qu’on ne comprenne pourquoi. Dans certains cas cela ne peut pas vraiment arriver. Mais dans d’autres ? Il suffit de prendre l’habitude de typer manuellement les fonctions pour éviter de se poser plein de questions pas forcément très intéressantes… Puisque F# est fortement typé, typez vos fonctions ! Ainsi la définition exemple deviendra la suivante pour s’assurer qu’elle ne traitera que des entiers :

let Add x : int  = x + 1
Add 5
// Sortie  :
// val Add : x:int -> int
// val it : int = 6

 

La console nous donne l’indication sur les types utilisés, c’est le sens de “val Add : x:int –> int”. Une fonction F# est un “citoyen de première classe”, c’est à dire qu’elle peut être manipulée exactement comme une valeur. C’est pourquoi elle retourne une valeur symbolisée par “val”. Ensuite la console nous dit que le paramètre “x” est de type entier et que sa transformation via la fonction produira un autre entier (int –> int).

Ensuite la valeur est calculée pour “Add 5”, et le résultat est 6 comme on s’y attend. On voit au passage que l’utilisation de la fonction ne demande aucun décors inutile, ni accolades, ni parenthèses ou point virgule superflu… Propre et concis comme je l’avais annoncé.

Mais pourtant typé et vérifié à la compilation. La preuve si je tente un “Add 8.0” désormais, la console génèrera l’erreur suivante (les tests sont faits sous Tsunami et non VS ici, mais cela revient au même) :

> > 
  Add 8.0;;
  ----^^^

stdin(38,5): error FS0001: This expression was expected to have type
    int    
but here has type
    float

 

Les fonctions : des méthodes avec un autre nom ?

Bien sûr que non. Si la nouveauté de F# se cachait dans un effet de terminologie rien n’aurait d’intérêt. Ainsi les fonctions de F# n’ont finalement que peu à voir avec les méthodes des objets. Bien entendu elles fixent des opérations, éventuellement séquentiellement, mais elles sont d’une nature différente.

Elles possèdent des propriétés qui les rendent uniques :

  • Une fonction donne toujours la même sortie pour une valeur d’entrée donnée
  • Une fonction n’a aucun effet de bord

 

Ces caractéristiques peuvent vous sembler drastiques ou floues ou les deux ! Mais elles sont tellement ancrées dans les mécanismes de F# et des langages fonctionnels que ces derniers font tout pour en assurer le respect.

Du fait de ces particularités les fonctions des langages fonctionnels n’ont que peu de choses à voir avec les méthodes des objets (type Java ou C#) et encore moins avec les procédures des langages impératifs procéduraux (type C ou Pascal).

Même entrée, même sortie

Le premier point de différence que nous avons vu consiste en cette règle inflexible : les fonctions fournissent toujours les mêmes valeurs de sortie pour les mêmes valeurs d’entrée. A un domaine correspond une étendue (range) et une seule, toujours identique, toujours selon le même mapping.

Dans les langages impératifs on voit les procédures ou les méthodes comme des morceaux de code qui “font” ou “calculent” quelque chose. Cela peut sembler étrange ou n’être que philosophique, mais dans un langage fonctionnel une fonction ne fait aucun calcul… C’est une simple opération de mapping (de mise en correspondance) entre le domaine d’entrée et l’étendue de sortie.

La fonction Add de notre exemple peut ainsi être vue comme série d’ordres associant une valeur d’entrée à une valeur de sortie. Pas besoin de calcul, la fonction pourrait parfaitement être écrite comme suit (en style C#) :

int add1(int input)
{ 
   switch (input)
   {
   case -1: return 0; 
   case 0: return 1;
   case 1: return 2;
   case 2: return 3;
   case 3: return 4;
   etc … à l’infini !
   }
}

 

Bien entendu une telle façon de coder est idiote puisqu’elle demanderait une liste de “case” aussi longue que l’étendue des entiers ! Remplacer les “case” par une opération d’addition résout le problème plus facilement et plus efficacement surtout. Mais le principe est là : une fonction F# ne fait pas de calcul, elle ne fait que faire correspondre un ensemble de sorties à un ensemble d’entrées.

Pas d’effet de bord

Les effets de bords font partie à la fois des risques et des avantages des langages impératifs. Parfois un bogue apparait à cause d’un tel effet de bord, parfois on recherche l’effet de bord. Par exemple la définition d’une propriété en C# exploite au maximum l’effet de bord : au lieu de stocker la valeur dans un champ, la propriété permet d’intercepter l’écriture, de tester si la valeur est identique ou non à celle du champ sous-jacent, de lancer des notifications (INPC notamment), ou tout autre traitement ! Ainsi basculer à “true” une propriété “Playing” d’un lecteur de média lancerait automatiquement la lecture de ce dernier. C’est un pur effet de bord. Et pourtant il est recherché et exploité en tant que tel. Hélas ces effets de bord peuvent aussi conduire à des bogues difficiles à trouver, à des instabilités inexplicables, etc.

Dans une fonction mathématique l’entrée et la sortie sont deux choses totalement différentes, les deux sont prédéfinies et immuables. La fonction ne “change” pas l’entrée en sortie, elle retourne juste une “correspondance”, une valeur préexistante de sortie pour toute valeur préexistante d’entrée.

Par nature une fonction ne peut pas modifier la valeur d’entrée ni avoir aucun effet sur rien d’autre. Rappelez-vous qu’une fonction n’est pas un calcul, c’est juste une table de correspondance élevée au rang d’élément central des langages fonctionnels !

L’immuabilité des valeurs est un aspect très important qui peut pourtant paraitre subtile ou même artificiel. Il n’en est rien. En mathématiques on ne s’attend pas à ce qu’un nombre soit changé subrepticement par une fonction, “à l’insu de son plein gréé” aurais-je envie d’ajouter ! Par exemple si je pose x =10 et que je pose l’opération y = x + 5 je ne m’attends certainement pas à ce que “x” ait été modifié par l’opération, au contraire je suis convaincu que x restera identique et que le calcul va me retourner “y” un autre nombre appartenant à l’étendue de sortie. Dans le monde des mathématiques tous les entiers préexistent déjà avant même que la moindre fonction ne soit écrite. Et une fonction n’est en réalité qu’une opération ensembliste qui met en relation des couples d’entiers au sein de l’ensemble des entiers (pour notre exemple).

Il est vrai que pour l’instant il est difficile de voir ce que cela change sur la façon de programmer des applications… Mais cela va venir.

Avantages des fonctions “pures”

Ce n’est pas la pureté en tant vertu qui nous intéresse ici mais bien la nature dépouillée des fonctions. De telles fonctions retournant assurément la même sortie pour la même entrée et ne possédant strictement aucun effet de bord sont dites “pures”, par définition. Et c’est quand on envisage l’effet de bord de cette absence d’effet de bord que les choses deviennent intéressantes paradoxalement !

En effet, avec les fonctions pures viennent un ensemble d’avantages du point de vue de la programmation :

  • Elles sont parallélisables de façon triviale. Je peux imaginer prendre un million d’entiers de valeur différente et utiliser un million de PC, tablettes, smartphones, GPU différents à qui je vais fournir la fonction “Add” de notre exemple et chacun des un millions d’entiers. Je vais pouvoir demander à ce million d’ordinateurs de calculer la fonction “Add” au même moment. J’obtiendrais immédiatement un million de résultats sans avoir une seule fois à gérer des locks, des protections de mémoire, des sémaphores, … Les fonctions sont parallélisables par nature sans rien faire de spécial. Elles se trouvent donc être parfaitement adaptées aux ordinateurs modernes qui contiennent des CPU multi-cœur et qui réclament du développeur une programmation multi-tâche pour en tirer parti !
  • Les fonctions peuvent s’utiliser de façon “paresseuses” (lazy comme dans le lazy loading), c’est à dire que je peux demander leur évaluations maintenant ou plus tard, cela ne changera rien aux résultats qu’elles retourneront. Les résultats ne dépendent jamais d’un “contexte”, ils ne dépendent que des valeurs d’entrée. Comme il n’y a aucun effet de bord, comme les résultats sont toujours identiques pour les mêmes entrées et que les valeurs de sortie sont différentes des valeurs d’entrée qui restent immuables, l’ordre dans lequel les fonctions sont exécutées pèse beaucoup moins sur la logique de l’application. Maintenant ou plus tard, cela ne changera pas l’état du logiciel.
  • Il est possible de ne calculer qu’une seule fois une fonction pour une valeur d’entrée et mettre celle-ci dans un cache. En effet, je suis certain que la même valeur d’entrée donnera toujours la même valeur de sortie. En cachant cette dernière j’éviterai de prochains appels à la fonction. Dans certains cas une telle approche pourra devenir un avantage important en terme de vitesse d’exécution.
  • Comme je le faisais remarquer un peu plus haut l’ordre d’exécution des fonctions pures n’a pas vraiment d’importance. Il en va de même si j’ai plusieurs fonctions : je peux les exécuter dans n’importe quel ordre cela ne peut avoir aucun effet sur les résultats tant que les entrées restent les mêmes.

 

J’en perçois quelques uns qui se disent que tout cela est bien abstrait et qu’en réalité si j’ai deux calculs à faire l’un après l’autre et que j’inverse l’ordre je n’aurai pas du tout le même résultat et qu’on peut le prouver en une seconde et demi… Bon, je ne dirais rien car cela prouve qu’ils suivent et je les en remercie… Mais il faut bien réfléchir au fait que pour le moment je parle de fonctions et de leur rôle dans un langage fonctionnel, je n’ai pas encore parler de programme… Dans un programme l’ordre d’appel des fonctions peut bien entendu avoir une importance, mais du point de vue d’un langage fonctionnel cela n’en a pas. Dans un langage impératif objet comme C# par exemple peu importe le logiciel qu’on écrit, on sait déjà à l’avance que la méthode représentant le Constructeur devra être exécuté AVANT tout autre méthode d’une instance de classe. L’ordre des méthodes ne peut pas être quelconque, il aura un impact sur le résultat, l’appel d’un destructeur à n’importe quel moment vous le prouvera aussi en une seconde et demi... C’est ainsi dans ce sens qu’il faut comprendre qu’au contraire, dans un langage fonctionnel l’ordre d’exécution des fonctions n’a aucune importance du point de vue même du langage. Pas facile au premier coup je l’accorde… Mais vous allez voir ça va s’éclaircir petit à petit (enfin je l’espère vu le mal que je me donne !).

Revenons aux constats que nous faisions quelques lignes plus haut et regardons ce qu’ils impliquent comme puissance particulière aux langages fonctionnels comme F# :

  • La programmation multitâche est simplifiée (immuabilité des valeurs, des résultats…)
  • L’évaluation “lazy” autorise des optimisations importantes
  • Cacher les résultats est appelé “memoization” et permet aussi des optimisations non négligeables
  • Ne pas s’occuper de l’ordre des évaluations rend la programmation parallèle plus simple et ne créé pas de bogues quand les fonctions sont réordonnées ou refactorées.

 

Il n’y a pas que des avantages à tout cela. Par exemple les valeurs d’entrées et de sorties sont immuables. On peut se demander comment faire quelque chose d’utile si on ne peut pas affecter de nouvelles valeurs à une variable… De même une fonction a exactement une entrée et une sortie. Là encore on peut s’interroger sur l’efficacité d’un langage où une fonction ne peut pas accepter zéro ou 5 paramètres par exemple.

En réalité il existe des moyens de contourner ces problèmes. Sinon vous vous imaginez bien qu’on ne pourrait pas se servir de F# et qu’il n’existerait même pas d’ailleurs.

Comme nous le verrons plus tard ce qui peut apparaitre comme des inconvénients sera même à la base d’une part de la puissance de F#. Mais chaque chose en son temps…

Les Valeurs

La notion de valeur en langage fonctionnel est essentielle car elle diffère de ce qu’on entends par ce vocable en programmation impérative.

Regardons à nouveau l’opération d’addition de l’exemple utilisé plus haut dans cet article :

let Add x = x + 1

 

Quelle est la signification de “x’ dans ce contexte ?

Il signifie deux choses :

  1. Accepter une valeur issue du domaine d’entrée
  2. Utiliser le nom “x” pour représenter cette valeur de telle façon à ce qu’on puisse s’y référer plus tard

 

Le processus qui consiste à utiliser un nom pour représenter une valeur est appelé “binding” (ligature) car le nom “x” est lié (bound) à la valeur d’entrée.

Je sais, le binding est un mot très utilisé depuis longtemps pour désigner bien autre chose sous XAML. Mais oubliez les autres langages pour l’instant et n’hésitez pas non plus à vous concentrer sur le sens anglais de ces mots, les choses seront plus claires. Un Binding XAML est aussi une ligature, un lien. Le mot “binding” est utilisé exactement dans le même sens sous F#. C’est nous, Français, qui par l’habitude de XAML nous en sommes fait une représentation mentale particulière. Pour un Anglais ou un Américain c’est un mot commun utilisé dans milles contextes différents… dont celui des langages fonctionnels pour désigner tout simplement le fait de donner un nom symbolique à une valeur…

Si nous continuons avec notre addition et son “binding” (x représente la valeur d’entrée) et si nous décidons d’exécuter ce code avec pour valeur d’entrée 10, en réalité le “x” sera remplacé par “10” partout et c’est comme si nous avions écrit :

let Add 10 = 10 + 1, d’où le résultat 11 …

Evident ? Non ! pas tant que ça. Car en ce moment même je suis certain que vous pensez à “x” comme un conteneur, un slot, une variable à laquelle la valeur sera assignée et qui pourra éventuellement prendre une autre valeur plus tard.

Erreur. Pardonnable puisque c’est justement ce que j’essaye de vous expliquer et qui est nouveau avec F#. En réalité il faut voir “x” comme un lien unique et immuable. C’est le “nom” du “10” de notre exemple. 10 s’appelle x et x représente 10 symboliquement. Je m’appelle Olivier Dahan, c’est mon “x”, il est associé à l’être humain que je suis, à son ADN unique, et ce nom ne pourra représenter dans le temps quelqu’un d’autre (évacuons les homonymes). Ma carte d’identité ne pourra pas être réutilisé à ma mort même par un homonyme d’ailleurs. C’est un lien “one way / one shot”, à la naissance, une “identité”. Pas un conteneur, pas une variable, pas un réceptacle pour un contenu variable.

Aussi subtile ou artificiel que cela puisse vous paraitre je vous demande de bien y réfléchir pour en saisir toutes les nuances car ce concept est tout simplement critique dans la façon de “penser fonctionnel” : il n’y a pas de “variables”, seulement des “valeurs”.

Les valeurs fonction

Le principe qui vient d’être énoncée au sujet des valeurs et de leurs noms (“binding”) est tout aussi vrai pour les fonctions elles-mêmes. De fait le nom “Add” de notre opération exemple est lui un binding vers “une fonction qui ajoute un à son entrée”. La fonction est indépendante du nom auquel elle est liée.

L’instruction “let Add x = x + 1” signifie “à chaque fois que tu vois le nom ‘Add’ remplace-le par la fonction qui ajoute un à son entrée”.

Ainsi, le nom donné à une fonction est un binding, un lien entre ce nom la fonction proprement-dite, ce nom particulier est appelé une valeur fonction.

C’est un point de vue qui là aussi peut sembler artificiel mais changer de paradigme réclame souvent un gros effort car certaines façons de voir les choses semblent ne rien apporter de neuf tant qu’on n’a pas fait l’effort de changer de point de vue, c’est un cercle vicieux. C’est justement là toute la difficulté de l’exercice, sinon tout le monde serait visionnaire ou au moins d’une sagesse infinie… Et ça se saurait.

Changer de point de vue réclame un effort, pas toujours par le “déplacement” lui-même qu’il faut effectuer, mais bien parce qu’on a une sorte de certitude que se déplacer ne changera rien… Changer de paradigme c’est arriver à faire bouger cette certitude pour accepter de changer de point de vue. Car une fois ce déplacement effectué, on voit en effet les choses autrement et la question de ce changement finalement ne se pose plus…

On peut facilement se convaincre que le nom et la fonction ne sont qu’un lien arbitraire et qu’il n’y a pas techniquement identité entre les deux :

let Add x = x + 1
let Plus = Add
Plus 10

 

Ce qui donnera comme résultat à la console (j’utilise toujours Tsunami ici, mais vous pouvez aussi utiliser Visual Studio bien entendu) :

> let Add x = x + 1
  let Plus = Add
  Plus 10
  ;;

val Add : x:int -> int
val Plus : (int -> int)
val it : int = 11

Les deux noms “Add” et “Plus” font référence à la même fonction. Certains langages impératifs peuvent autoriser des constructions de ce type (les noms sont des symboles représentant des pointeurs) mais dans un langage fonctionnel il s’agit d’un mécanisme de base non d’une astuce, d’une ruse ou d’une extension de la norme originale.

Toute fonction peut être identifiée grâce à sa signature qui a la forme standard suivante :

domain –> range

La syntaxe générique d’une fonction est ainsi :

val NomFonction : domain –> range

Les valeurs dites simples

Pour rendre les choses plus claires partant du cas le plus élémentaire possible, celui d’une opération qui retournerait toujours 5 et qui ne prendrait en charge aucun paramètre d’entrée :

image

Comment écrire une telle opération “constante” en F# ?

let MaConstante = 5

ce qui, une fois évalué, retournera

val MaConstante : int = 5

Aucune flèche de mapping cette fois-ci, uniquement une valeur entière, 5. Ce qui est différent c’est la présence d’un signe égal avec la valeur écrite à sa suite. Le compilateur F# sait que ce binding (le nom “MaConstante”) possède une valeur connue et il va la retourner à chaque fois qu’on utilisera ce nom.

Bien qu’elle soit traitée d’une façon identique la différence entre une valeur fonction et une valeur simple se voit immédiatement à la console grâce à la présence du signe égal dans le cas de la valeur simple là où une flèche est utilisée pour une valeur fonction.

Ces notions peuvent sembler encore une fois très rudimentaires, il est vrai que la notion de variable nommée ou de constante nous est très familière depuis des lustres en informatique. Mais c’est dans le détail que se situe la différence énorme. F# est un langage fonctionnel, donc basé sur des fonctions, ces dernières ne faisant pas de calcul mais réalisant un mapping entre un domaine d’entrée et une étendue de sortie, les variables n’existent pas puisque toute valeur est immuable, et ces valeurs peuvent être des constantes ou des fonctions…

Vous voyez que lorsqu’on résume tout ce que nous venons de voir pour le moment cette longue phrase décrit quelque chose de nouveau qui n’a rien à voir avec C# ou C++ ou n’importe quel langage impératif même si à chaque étape il nous a semblé que peu de choses voire rien ne  changeait… Changer de “paradigme” c’est arriver à voir cette nuance.

Valeurs simples contre Valeur fonction

Comme je le disais les valeurs simples et les valeurs fonctions sont traitées de façon quasi identique par le compilateur F#. Cela marque une différence énorme avec les autres langages comme C# par exemple. Les deux types de valeur peuvent être bindés à un nom (en utilisant le not clé “let”) et les deux peuvent être passés en paramètre.

Et il est vrai que l’un des aspect clé des langages fonctionnels c’est exactement cela : les fonctions sont des valeurs qui peuvent être passées comme des entrées à d’autres fonctions.

Valeurs contre Objets

Dans les langages fonctionnesl la plupart des choses sont appelées des “valeurs”. Dans un langage orienté objet la plupart des choses sont appelés des “objets”. C’est le cas respectivement de F# et C#. En dehors du lexique propre à chaque langage quelle est la différence entre une valeur F# et un objet C# ?

Une valeur, comme nous l’avons vu n’est qu’un élément d’un domaine. Le domaine des entiers, celui des chaînes de caractères, le domaine des fonctions qui mappent des entiers vers des chaînes, etc. En principe les valeurs sont immuables. Sans oublier que les valeurs n’ont aucun comportement attaché.

Un objet, dans sa définition habituelle, est l’encapsulation d’une structure de données avec ses comportements associés (les méthodes). Généralement les objets ont des états (qui sont modifiables) et toutes les opérations qui modifient l’état interne doivent être fournies par l’objet lui-même (notation par le “point”).

En F# même les valeurs primitives ont des comportements de type objet. Par exemple il est possible par la syntaxe du “point” d’accéder à la longueur d’une chaîne de caractères :

”abc”.Length

retournera : val it : int = 3

Même si cela ressemble bigrement à de la programmation objet ce n’en n’est pas. Et on préfère dans le monde des langages fonctionnels comme F# réserver le nom d’Objets aux véritables instances de classes ou à d’autres valeurs qui expose des méthodes (F# autorisant la programmation orientée objet comme C# autorise un peu de fonctionnel).

La nuance entre objet et valeur peut sembler floue pour l’instant. Entrer en profondeur de chaque nuance ferait perdre le fil de la présentation du langage. Nous y reviendrons forcément et cela s’éclaircira alors. Posons pour le moment qu’une valeur n’est pas objet. Mais même sous F# il est possible de déclarer des classes comme en C# et donc d’en créer des instances qui sont alors appelées objets. Les valeurs F# peuvent proposer des opérations accessibles via la notation par point, cela n’en fait pas des objets dans le sens précisé ici.

Nommer les valeurs

Les noms de valeurs F# sont formés par tout caractère alphanumérique, incluant le caractère de souligné. Cette règle est la même qu’en C#.

Toutefois il existe quelques possibilités non offertes par C# comme par exemple l’utilisation de l’apostrophe. On le comprend aisément quand on repense au formalisme mathématique qui a fortement marqué les langages fonctionnels. En mathématique on utilise souvent le “prime” ou “seconde” sur des noms d’inconnues, de constantes, de fonctions… l’apostrophe ne peut apparaitre en première position toutefois. De fait, en F# les noms suivants sont valables :

  • a’b’c
  • Begin’
  • Ja’kin
  • Col’
  • Triangle_A’’

etc…

Le code suivant est tout aussi valable :

let Alpha = x
let Alpha' = dérivée de Alpha
let Alpha'' = dérivée de Alpha'

// définir des variantes de mots clés est possible:
let if' b t f = if b then t else f

// le double backstick est utilisable pour créer des noms
``ceci est un nom``
``123`` // nom valable aussi
let ``begin`` = "begin"

// s'utilise aussi pour simuler un langage humain dans des règles
let ``Client pour la première fois ?`` = true
let "Ajouter un cadeau à la commande`` = ()
if ``Client pour la première fois ?`` then ``Ajouter un cadeau à la commande``

// pour les tests unitaires
let [<test>] ``Quand l'entrée est 4 la sortie est le carré 16`` = 
	// code

let [<EtantDonné>] ``J'ai (.*) N produits dans mon cart`` (n:int) =
	// code

 

Personnellement je ne recommande pas l’utilisation de cette particularité mais chacun fait comme il préfère, l’essentiel étant que le code reste lisible et maintenable. Notamment l’utilisation dans les règles d’affaire fonctionne bien à condition d’être anglophone, en français on est obligé à un moment donné de mixer français et anglais (comme le “= true” et non “= vrai”) ce qui fait perdre beaucoup d’intérêt et de lisibilité. Le caractère backtick se trouve à gauche du 1 sur les claviers US, en France on l’obtient par un ALT-GR sur la touche du 7 / è, ce n’est pas ultra pratique, un peu comme les accolades {} de C# … Les américains et la localisation cela a toujours fait 2 … ils utilisent des symboles accessibles facilement uniquement sur leurs claviers.  Bref en plus de créer un code bizarre et pas forcément lisible, cette notation est enquiquinante à frapper.

Concernant les règles de nommage de F# on notera que par défaut les puristes utilisent un nommage à la camelCase et non un PascalCase comme… le Pascal ou C#. Pour tout ce qui doit être exposé à d’autres langages .NET, tous les types et noms de modules utilisent aussi le PascalCase. A vous de voir. Personnellement je préfère l’utiliser partout pour rester cohérent.

S’agissant de convention chacun fait ce qu’il lui plait tant que cela est constant dans le temps…

Types et fonctions

Intéressons-nous maintenant à la façon dont les types et les fonctions cohabitent. Comme je l’ai déjà expliqué F# est un langage fortement typé mais sa syntaxe autorise souvent à ne pas indiquer de noms de types car l’inférence de type est un comportement très apprécié du compilateur F#.

Mais il est souvent souhaitable ou indispensable de préciser les types, que cela soit pour des domaines d’entrées ou des étendues de sorties.

Par défaut le code suivant ne possède pas d’indication de type car ce dernier peut être inféré par le compilateur autant pour les entrées que pour les sorties :

let intToString x = sprintf "x is %i" x  // formate int en string
let stringToInt x = System.Int32.Parse(x)

 

Les  signatures par défaut seront :

val intToString : int –> string
val stringToInt : string –> int

Types primitifs

F# fourni les types primitifs les plus basiques : string, int, float, bool, char, byte, etc, plus de nombreux types dérivés du framework .NET.

Comme indiqué plus haut il est parfois nécessaire de préciser le type d’une valeur, ne serait-ce que pour clarifier son utilisation. Il est donc possible d’écrire le code suivant :

let intToFloat x = float x // convertir des entiers en flottants
let intToBool x = (x = 2)  // vrai si x égal 2
let stringToString x = x + " world" // concaténation

// dont les signatures sont :
val intToFloat : int -> float
val intToBool : int -> bool
val stringToString : string -> string

 

Annotations

Dans l’exemple ci-dessus le type est correctement inféré par le compilateur. Parfois ce n’est pas le cas. Par exemple le code pourtant simple suivant va provoquer une erreur :

let stringLength x = x.Length

//erreur : 
(* stdin(77,22): error FS0072: Lookup on object of indeterminate type based 
on information prior to this program point. A type annotation may be needed 
prior to this program point to constrain the type of the object. 
This may allow the lookup to be resolved. *)

 

L’erreur de compilation se produit car F# ne sait pas inférer le type de “x”, la seule définition que nous avons écrite ne le permet pas. De fait il est impossible de savoir si “Length” est une méthode valable dans le contexte.

De fait elle ne le sera que si “x” est une chaîne de caractères. Il est donc important de préciser ici le type pour éviter l’erreur de compilation :

let stringLength (x:string) = x.Length 

 

On peut de la même façon s’assurer du type de l’étendue de retour :

let stringLengthAsInt (x:string) :int = x.Length

 

On notera la différence de syntaxe. Dans le premier cas on ne fixe que le type du paramètre (le domaine d’entrée) en ajoutant deux points et le type sans oublier les parenthèses (sinon le type indiqué serait compris comme celui de la sortie et non de l’entrée). D’ailleurs dans le second cas où l’on fixe aussi le type de l’étendue de sortie on note la même syntaxe (deux points suivi du type) mais sans parenthèses cette fois-ci.

Utiliser des fonctions comme paramètre

Une fonction qui prend d’autres fonctions en paramètre ou qui retourne une fonction est dite d’ordre supérieur par opposition aux fonctions de premier ordre (qui n’ont pas ces caractéristiques). En anglais on parle de High Order Functions dont l’abréviation est HOF.

Ce que nous avons vu de F# jusqu’à maintenant est très simple. Forcément, pour faire des choses vraiment utiles il faut des constructions plus sophistiquées. Les fonctions d’ordre supérieur sont donc très communes dans la réalité.

Voici un exemple qui demande parfois un peu de temps pour arriver à secouer les neurones qui sont bien cachés tout au fond de la boite crânienne :

let eval fn = fn(5) + 2
let add x = x + 1
eval add

 

Le résultat de la console sera :

val eval : fn:(int -> int) -> int
val add : x:int -> int
val it : int = 8

La réponse est donc 8. je vous dirais que c’est 42 que ça serait certainement tout aussi ésotérique à part pour certains lecteurs déjà rompus à la façon de penser fonctionnel.

Dans notre code exemple ultra court qu’avons nous demandé au compilateur ?

La première ligne définit une fonction “eval” cette fonction prend un paramètre appelé “fn” ce qui est tout à fait arbitraire. Le choix de “fn” n’est toutefois pas innocent puisque cela nous rappelle que ce paramètre attendu est lui-même une fonction. Pourquoi ? Simplement parce qu’il est utilisé comme une fonction dans la définition de “eval” : “fn(5) + 2”.

La fonction “eval” est ainsi une fonction acceptant un paramètre qui est lui-même une fonction qui traite des entiers et retourne des entiers (déduit par inférence puisque nous avons écrit “fn(5)”). Le travail réel de “eval” est d’appeler la fonction en paramètre en lui passant comme paramètre un entier de valeur 5 puis d’ajouter 2 au résultat.

La signature de “eval” est donc eval : fn:(int->int) –> int

Ensuite nous définissons une fonction “add” comme dans les exemples précédents. Elle se propose d’ajouter 1 à la valeur d’entrée pour créer la valeur de sortie.

Cette fonction a une signature qui est ainsi add : x:int –> int

On remarque que cette signature est compatible avec celle inférée pour le paramètre fonction de “eval” …

Dès lors il nous est possible de demander l’exécution de “eval” en lui passant en paramètre “add”.

La console nous rappelle les signatures et nous donne le résultat, 8. En effet l’appel de “eval” avec pour paramètre “add”, correspond à évaluer add(5) + 2, donc 6+2 soit 8 – et pas 42 même si c’est la réponse à tout Sourire – Pigé ?

Vous commencez à entrevoir ce qu’est la programmation fonctionnelle… C’est très mathématique, c’est une façon de penser la solution très différente de l’impératif. On pourrait imaginer dans notre exemple que la fonction de base obtient des données depuis un service Web et que la fonction passée en paramètre est un traitement sur ces données et que, bien entendu, on pourrait avoir toute une batterie de telles fonctions suivant les traitements à effectuer. Cette précision rend peut-être un peu plus concret ce qui reste pour le moment encore un peu abstrait … La programmation fonctionnelle est très différente donc. Est-ce mieux ou plus facile ? Je ne crois pas. Être “mieux” dans l’absolu n’a pas de sens, et même en relatif encore faudrait-il définir les critères, quant à plus facile j’ai de gros doutes. En tout cas c’est très différents comme je vous l’avais annoncé ! Il faut bien en effet changer totalement de point de vue sur la façon de programmer la solution à un problème pour être capable d’écrire du code F#. Il y a bien un changement de paradigme, net. Ce n’est pas juste une expression galvaudée. Et encore n’en sommes nous qu’aux rudiments de F# et n’avons nous écrit que trois lignes de code ! Imaginez penser toute une application de la sorte… C’est à la fois effrayant au départ et très stimulant. En tout cas c’est ce que j’ai ressenti au début. Effrayant car ce n’est pas une façon habituelle de penser un code et qu’on se demande comment résoudre de véritables problèmes de cette nouvelle façon, et c’est très stimulant justement pour la même raison : il va falloir secouer les neurones !

On retrouve bien une différence aussi importante entre C# et F# qu’il peut y en avoir entre C et C#. Le passage de l’impératif simple à l’orienté objet a demandé un effort important et beaucoup de développeurs au départ n’en voyaient pas l’intérêt. Le passage de C# à F# n’est pas facile mais on y trouve les mêmes bénéfices que de C à C#.

Au début tout semblait très facile, voire naïf. Je le sais, certains d’entre vous se sont certainement dit que vraiment c’était trop simple, que je parlais comme à des grands débutants… La notion de variable, de constante, de valeur d’entrée, de paramètre de fonction… tout cela sonne comme déjà connu. En faire un article parait presque enfantin. Je me suis convaincu que certains ont simplement décroché. Tant pis pour eux ! Et puis, comme je l’annonçais, l’amoncèlement des petites différences qui apparaissaient si subtiles, voire mineures et sans intérêt, finit par créer un langage totalement nouveau qui force à penser autrement. C’est un peu comme dans un sketch de Devos, au début tout semble normal et petit à petit il vous emmenait dans un autre monde souvent absurde et délirant. Sauf qu’ici je vous amène lentement mais surement à F# qui n’a rien, absolument rien, d’absurde ni de délirant !

Les fonctions comme étendue de sortie

Penser qu’une fonction peut être un paramètre du domaine d’entrée n’est finalement pas si compliqué. Certains langages comme C# l’autorise d’une façon ou d’une autre. Mais envisager qu’une fonction soit le résultat d’une fonction est déjà plus curieux car il s’agit ni plus ni que de générer du code… sans passer par le compilateur, juste naturellement comme feature de base du langage…

Passons à l’exemple immédiatement, si le précédent vous a déjà chamboulé les connexions neuronales celui-ci risque de faire quelques courts-circuits !

Imaginons ainsi une fonction qui est capable d’en créer une autre. Car c’est cela que veut dire utiliser une fonction comme étendue de sortie d’une fonction.

Imaginons que cette fonction s’applique à générer des “additionneurs” c’est à dire des fonctions qui ajoutent un certain nombre à leur paramètre d’entrée. Elle s’écrira de cette façon :

let adderGenerator numberToAdd = (+) numberToAdd

 

Nous créons ici la fonction “adderGenerator” qui prend en paramètre “numberToAdd” et qui effectue l’opération ‘'(+) numberToAdd”.

La signature de cette fonction est val adderGenerator : int –> (int –> int)

Elle prend en effet un entier en paramètre et retourne une fonction qui transforme un entier en un autre entier…

Equipés de générateur d’additionneurs nous pouvons écrire :

let add1 = adderGenerator 1
let add2 = adderGenerator 2

 

C’est à dire que nous définissons deux fonctions, add1 et add2 comme étant le résultat de la fonction adderGenerator avec pour paramètre 1 puis 2.

adderGenerator va ainsi créer deux nouvelles fonctions, l’une ajoutera 1 à son paramètre d’entrée, l’autre 2. Les signatures de ces fonctions sont

val add1 : (int -> int) val add2 : (int -> int)
 

 

Le plus amusant reste à venir puisque maintenant nous pouvons écrire le code suivant :

add1 5    // val it : int = 6
add2 5    // val it : int = 7

 

En commentaire nous trouvons ce qui sera affiché par la console comme résultat. L’appel à add1 avec 5 en paramètre retournera bien 6. Alors que add2 retournera 7 (5 plus 2).

C’est, comment dire … “sportif”. Nous écrivons une fonction qui génère de façon paramétrique d’autres fonctions que nous pouvons ensuite utiliser sans compilation ni interprétation (visible en tout cas – on comprend mieux la présence d’une console interactive F# et pas pour C#). Le seul moyen de faire cela en C# est d’utiliser les services du compilateur ou d’utiliser un langage de scripting. En F# c’est une façon naturelle et spontanée de résoudre un problème donné.

Conclusion partielle

Passer des quelques mots de cette approche simplifiée de F# à la capacité bien rodée d’écrire toute une application n’est certainement pas “immédiat”, c’est un euphémisme… Le changement de paradigme devient maintenant tellement radical que cette expression que vous pensiez un peu exagérée au départ vous en regrettez presque désormais toute la force et la signification… De joyeuse plaisanterie présumée de la nouveauté qui n’en est pas une à la réalité du bouleversement impliqué par ce que vous venez de lire il y a comme un vertige qui vous traverse.

C’est bien. C’est normal. Cet article ne veut qu’obtenir ce résultat. Ce n’est pas un livre sur F#, juste une initiation à l’esprit de la programmation fonctionnelle avec F#. Il faut ressentir ce vertige, cela prouve que vous avez compris le saut à faire et le vide qui s’étire entre vos habitudes actuelles et celles à prendre pour écrire correctement en fonctionnel.

Comme je l’avais expliqué le but n’est d’ailleurs pas de vous amener à programmer en F#, juste à vous faire toucher cette forme particulière de programmation pour vous aider à mieux programmer en C# qui intègre des notions fonctionnelles. Et peut-être, peut-être seulement, pour les plus bouleversés d’entre vous pourquoi pas alors utiliser réellement F# …

Mais pour l’instant il nous reste du pain sur la planche. Une partie 2 annonce une partie 3 à venir !

Le temps de souffler un peu pour moi et aussi pour vous, et on s’y remet ! alors…

Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus