Publié le 22/06/2017 Par Gaël Dupire

Réaliser un programme c’est surtout manipuler des données. Il est donc recommandé de disposer d’entités spécialisées pour chaque cas d’utilisation de ces données. Cela tombe bien, les familles de langage offrent différentes manières de créer un type. Et la programmation fonctionnelle n’échappe pas à cette règle. Vous allez le voir, dans certains cas, ces “types” fonctionnels sont même des amis précieux. Exemples en F# et en Scala.

Lorsque l’on écrit du code, les définitions courtes facilitent la compréhension, car elles nécessitent peu d’éléments de syntaxe. La plupart du temps, on peut en prendre connaissance d’un seul coup d’œil. Cette concision encourage les bonnes pratiques de codage et garantit une bonne maintenabilité. C’est un des grands points forts des langages fonctionnels. Qui plus est, le typage est expressif. Il permet de développer des relations qui n’existent pas naturellement dans les langages-objets. Il permet de traiter efficacement et sans avoir à “lancer” d’exception pour pallier des interactions entre plusieurs systèmes. Il permet également d’éviter l’écueil de la programmation-objet qui autorise la représentation concrète d’état qui devrait être impossible.

Un grand nombre de langages fonctionnels (F#, Scala et OCaml par exemple) sont en réalité multi-paradigmes. S’ils sont orientés “fonctionnel”, ils offrent la possibilité, de créer des types orientés “objet” dès que le besoin s’en fait sentir. On peut notamment utiliser des interfaces et de l’héritage. Une bonne pratique consiste même à encapsuler dans un objet les états mutables dont on peut avoir besoin afin de ne pas laisser directement leur accès disponible. Mais voyons cela plus en détail en F# et en Scala.

L’avantage clef du langage fonctionnel

Pour commencer, notez que les langages fonctionnels offrent de puissants systèmes d’inférence de type. C’est-à-dire que le compilateur est capable de déterminer dans un grand nombre de cas le type manipulé au sein d’une expression (il s’agit de l’unité de syntaxe qui associe à une valeur sa définition). Par exemple, dans l’expression suivante en F# :

let a = 3

Et en Scala

val a = 3

Le compilateur est capable de déterminer que “a” est un entier. Inutile donc de déclarer son type. C’est une fonctionnalité puissante qui permet, si on s’appuie dessus correctement, de gagner un temps précieux pendant le développement.

Les alias de type pour éviter les “primitive obsession

En F# et en Scala, on peut définir des alias de type. La syntaxe est la suivante en F# :

type quantity = int

Et en Scala

type quantity = Int

Remarquons que la seule différence entre les deux langages se limite à la manière dont le type entier est défini. On définit de cette façon un synonyme que l’on peut manipuler comme n’importe quel type. Il n’y a pas de surcoût à l’utilisation. Cette possibilité est très utile pour clarifier le sens effectif d’un élément du code. C’est une façon d’éviter à moindres frais les “primitive obsession” (pour un bon traitement du sujet on peut voir ce post). Toutefois, le type synonymisé peut être utilisé en remplacement du synonyme ce qui limite un peu l’effet.

Les types de données algébriques pour obtenir flexibilité… ou persistance

Les types de données algébriques tirent leur nom des propriétés qui sont exploitées pour les définir. Ce sont des produits cartésiens de données. Dans cette famille de types, on peut compter sur les tuples (n-uplet). Très flexibles, ils permettent de définir à la volée la composition du type. Ils se révèlent utiles quand on veut faire sortir plusieurs valeurs d’une fonction, par exemple. Ils se définissent de cette façon en F# :

let tuple = (1, "Hello world !!",3.1415)

Et en Scala

val tuple = (1, "Hello world !!",3.1415)

Ici le type inféré sera int*string*float en F# et (int, string, float) en Scala. Grâce à ce type, on peut définir un nombre arbitraire de composantes de type différent. Rapide et efficace.

L’autre membre de la famille des types de données algébriques, ce sont les enregistrements, ou “record”.. Ils ont pour eux d’être persistants et conviennent bien quand on veut rapidement créer et manipuler ses propres types de données. Si l’on souhaite créer une entité porteuse de champs nommés, on utilise des enregistrements. En F#, on écrit :

 
type ComplexNumber = { RealPart : float ; 
                      ImaginaryPart : float }

En Scala le record n’est pas natif, mais on peut trouver sur github une librairie le définissant ici. On peut cependant définir nativement des entités équivalentes de la façon suivante :

 
case class ComplexNumber(realPart: Float, imaginaryPart: Float)

Ce type représente un nombre complexe. On le compose avec deux “float”. Dans cette déclaration, le compilateur ne peut pas inférer les types, il faut donc lui indiquer le type de chaque champ. L’inférence de type est puissante, pas magique ! On obtient alors un type de données à la fois persistant, pratique et rapide à définir. Il bénéficie par ailleurs d’une petite optimisation efficace pour éviter toute la lourdeur d’une recopie complète lorsque l’on souhaite seulement remplacer une partie des valeurs composant l’enregistrement, le “Copy on Write” (COW). Cela consiste à conserver les références des composantes qui ne sont pas modifiées.

 
type quantity = { quantity : int }
type price = { price : float } 
type commandLine = { itemName : string ; 
                     itemQuantity : quantity ; 
                     itemPrice : price }
let l1 = { itemName = "Raspberry" ; 
           itemQuantity = { quantity = 1 } ; 
           itemPrice = { price = 4.99 } }
let l2 = { l1 with itemName = "Apple" }

let b1 = System.Object.ReferenceEquals(l1.itemName,l2.itemName)
let b2 = System.Object.ReferenceEquals(l1.itemQuantity,l2.itemQuantity)
let b3 = System.Object.ReferenceEquals(l1.itemPrice,l2.itemPrice) 

et en Scala, on a un code équivalent de la façon suivante :

 
type Quantity = Int
type Price = Float
case class CommandLine(itemName:String, itemQuantity: Quantity, itemPrice:Price)
val l1 = CommandLine("Raspberry", 1,4.99f)
val l2 = l1.copy(itemName = "Apple")
l1.itemName.eq(l2.itemName)
l1.itemQuantity.eq(l2.itemQuantity)
l1.itemPrice.eq(l2.itemPrice)

Dans ce code, on crée une instance de record de type CommandLine l1. Ensuite on souhaite modifier le champ ItemName. Pour cela on assigne la valeur l2 avec la syntaxe utilisant le with en F# et le copy en Scala. Son  style, très descriptif, explicite l’intention (l2 c’est l1 avec la valeur “apple” en itemName). Vous trouverez l’implémentation F# de ce code dans le projet de test accessible ici. Lisez le wiki avant de l’utiliser !

Plus que l’aliasing de type, le type record est particulièrement indiqué pour éliminer le “code smell: primitive obsession”. Son design est rapide. Notez que son nom portera l’information de ce qu’il représente. Il apportera de la clarté dans la signature des méthodes. Par ailleurs, le fait qu’il ne puisse pas être remplacé par le type encapsulé permettra d’éviter les erreurs d’inversion d’arguments dans la signature de la méthode. Enfin, indiquons que le surcoût de l’encapsulation est nul grâce aux optimisations de compilation. Alors, pourquoi s’en passer ? Vous trouverez un petit benchmark relatif à ce propos dans le projet de test que j’ai présenté plus haut et dont revoici le lien. Pensez à lire le Wiki avant de l’utiliser.

Les “union type”, un type addi(c)tif

Les types additifs (“union type” en anglais) sont certainement les plus déstabilisants lorsque l’on vient de la programmation-objet. Il s’agit de la réunion de plusieurs aspects d’une même entité dans un seul type. Cependant, chaque instance du type ne peut être représentée que par un seul de ses aspects. En programmation-objet, on utilise les interfaces pour définir un aspect. Dans ce cas, une classe peut implémenter plusieurs interfaces et donc être représentée par plusieurs de ses aspects.

Prenons un exemple et essayons de rendre nullable un type qui ne l’est pas. Ici la valeur est soit une instance d’un type, soit une valeur représentant la nullité. Voilà la réunion des aspects dont nous avons besoin pour définir notre type. En F#, on le définira de la façon suivante :

 
type nullable<'a> =
    | Value of 'a
    | Null

Ici ‘a représente le type que l’on veut rendre nullable. Il s’agit de polymorphisme paramétrique. On peut donc être soit une Value soit un Null (de type ‘a!!). En C#, cela pourrait donner ceci :

 
public struct Nullable where T : struct
{
    private T _internalValue;
    private bool _hasValue;

    public bool HasValue { get { return _hasValue; } }

    public T Value
    {
        get { return _internalValue; }
        set
        {
            internalInitialization(value);
        }

    }

    public Nullable(T initValue)
    {
        _internalValue = initValue;
        _hasValue = true;
    }

    private void internalInitialization(T initializationValue)
    {
        _internalValue = initializationValue;
        _hasValue = true;
    }
}

L’implémentation utilisée dans le namespace System de la librairie de base du C# se trouve sur le site referencesource de Microsoft. Quelle est la différence entre les deux implémentations me direz-vous ? En C#, la structure contient l’information sur la nullité ainsi que la valeur. En F#, elle force une entité à n’être que nulle ou comme ayant une valeur.

L’inconvénient dans l’implémentation C# tient au fait que même si HasValue retourne false, rien n’empêche de récupérer la valeur de l’entité. Cette dernière n’ayant pas été initialisée, on va donc récupérer la valeur par défaut de T. Finalement, on a donné une représentation à un état interdit, ce qui est impossible avec l’implémentation en F# (qui est bien plus simple et bien plus courte). Un bémol tout de même puisque l’on peut empêcher l’utilisation de l’état interdit dans l’implémentation C# en lançant une exception si HasValue vaut true et que l’on veut accéder au champ Value. Il n’en reste pas moins que l’on peut tenter d’accéder à la valeur sans que cette dernière soit concrète.

Dans son blog Scott Wlaschin, parle du paradigme make illegal state unrepresentable qui est imputable à Yaron Minsky sur le blog de Jane street. Vous trouverez d’ailleurs beaucoup de ressources concernant le OCaml sur ce blog. Je vous recommande également le livre auquel il a contribué, real world OCaml.

Pour manipuler ces types, il faut utiliser le filtrage par motif (pattern matching). Par exemple pour appliquer une fonction f qui transforme un a en b on utilise le code suivant :

 
let apply f = 
    match f with 
    | Value a -> f a |> Value
    | Null -> Null

On constate que chaque aspect doit être traité séparément. Si on en oublie un, le compilateur nous le fera remarquer. Au premier abord, on peut être rebuté par cette façon de faire. En réalité c’est ce qui fait une des grandes forces des langages fonctionnels, car même si cela ne saute pas aux yeux tout de suite il ne s’agit pas ici de faire un simple switch sur les valeurs. De plus, ces types supportent la récursivité. C’est-à-dire qu’ils peuvent se référencer eux-mêmes. Ainsi pour créer une pile, on peut utiliser le type suivant :

 
type stack<'a> = 
    | Node of 'a*stack<'a>
    | Nil

Si nous avons déjà une valeur du type stack<‘a>, on peut ajouter un élément au haut de cette pile avec la fonction cons que l’on définit de la façon suivante :

 
let cons node stack = (node,stack) |> Node

Ce qui est intéressant dans cet exemple c’est que cette opération préserve naturellement la mutabilité, car on crée un nœud avec une référence à notre élément et une autre pile sans rien modifier. Si on avait souhaité ajouter un élément en fin de liste, l’opération aurait été plus périlleuse, car nous aurions été obligés de modifier la liste en bout de chaîne. Cette structure de liste chaînée LIFO est une structure de base présente dans de nombreux langages fonctionnels. En F# il s’agit du type list<‘a>. La fonction cons est souvent représentée en utilisant l’opérateur “::”.

Les options pour remplacer les null

En F#, plutôt que devoir gérer des null qui se révèlent parfois compliqués, on utilise plutôt les options. Elles peuvent se définir de la façon suivante :

 
type option<'a> =
    | Some of 'a
    | None

let maybeString1 = Some("Hello world !!!")
let maybeString2 = option.None

Ce type est un classique des langages. On peut le trouver sous différents noms. En Scala par exemple :

 
val maybeString1: Option[String] = Some("Hello world!!")
val maybeString2: Option[String] = None

En haskell, il s’agit du type Maybe.

Soit. Mais les options “in real life” ? Une application immédiate visible dans ce système de typage concerne le retour d’argument d’une méthode. On peut utiliser le même principe que les options pour donner accès à une ressource tierce, typiquement un accès en base de données.

 
type DBConnectionResult =
    | Result of IDBConnection
    | Error of string

Dans cet exemple, IDBConnection est l’entité qui va nous permettre d’avoir accès à la base de données. La fonction qui va faire la connexion ne nous retourne pas directement cette entité, mais plutôt un DBConnectionResult. On peut ainsi traiter le cas où la connexion n’a pas pu être récupérée. Dans ce cas, on récupère un DBConnectionResult.Error contenant une string. Cette chaîne de caractère contiendra la raison pour laquelle la connexion n’a pas pu être récupérée. Idéal pour traiter facilement les effets de bord dépendant de systèmes extérieurs au nôtre. En Scala c’est plus compliqué. Nativement, on ne peut pas le faire. En revanche, on peut utiliser les cases object décrites ici ou en utilisant des types non boxés comme .

Sinon, il existe le Haskell

Pour conclure. Ici nous avons utilisé le F# et le Scala pour écrire nos exemples de code, car ce sont, à notre sens, les plus accessibles en termes de plateforme de développement. Le Haskell étant un langage purement fonctionnel, il est impossible de déclarer des types orientés objet. Il bénéficie cependant d’un système assez proche de celui des interfaces, en beaucoup plus puissant. Ce qui rend son utilisation particulièrement intéressante.

——–

Si le sujet vous intéresse, ci-dessous, les autres articles du dossier sur la programmation fonctionnelle, co-écrit avec Clément Carreau :
La programmation fonctionnelle, un nouvel espoir
– Le Pattern Matching, le Demolition Man intelligent

Pas encore de commentaires

Publier un commentaire

Auteur

Gaël Dupire

Gaël a débuté sa carrière en tant que Software Consultant avant de devenir Senior Software Development Engineer à la Société Générale (SGCIB) puis chez Meritis il y a 10 ans.

Gaël est diplômé de l’UTC (Doctorat de Mathématiques appliquées).

Il est passionné par les maths et leur utilisation, Il adore coder, aussi bien en .Net, python, C++ ou Matlab.