Les types en Elm permettent d'utiliser des types génériques, ce qui apporte généralement de la souplesse à une interface.
Par exemple, le type Maybe a peut contenir une valeur de n'importe quel type.
type Maybe a = Nothing | Just a
Note que dans l'exemple ci-dessus, le paramètre de type a est utilisé des deux côtés du signe égal =.
On dit alors que a est lié à des données au sein de la définition du type.
Dans certains cas, cependant, le paramètre de type n'apparaît que du côté gauche de l'équation.
type Distance unit = Distance Float
Dans la définition de Distance ci-dessus, unit est un paramètre de type libre, lié à aucune donnée du type.
On peut aussi appeler ce type un type fantôme.
C'est étonnamment utile lorsqu'on veut imposer des contraintes au moment de la compilation. Par exemple, on veut s'assurer qu'on n'additionne que des distances de même unité.
-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)
-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float
-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot
-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0
-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048
-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)
Le code extérieur à ce module ne peut pas accéder à l'intérieur du type Distance.
Les utilisateurs peuvent additionner deux Distance Meter, mais pas un Distance Meter avec un Distance Foot.
import Distance exposing (Distance)
-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter
-- Does not compile
errDist = Distance.add Distance.meter Distance.foot
Rien n'impose de restriction sur les types qui peuvent être fantômes, et les enregistrements peuvent l'être aussi. Lorsqu'ils servent de types fantômes, les enregistrements permettent d'exprimer des contraintes complexes qui peuvent se transformer au fil des fonctions.
Ajoutons une nouvelle unité de distance dans le module Distance, appelée LegoBlock.
Les blocs physiques, en tant qu'objets réglementés, ont toujours deux propriétés : leur distance est non fractionnaire et non négative.
type LegoBlock = LegoBlock
fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0
nonFractional et nonNegative sont les champs d'un enregistrement qui n'existe pas en dehors d'un argument de type. Il est donc logique de leur donner le type (), appelé le type unité, qui ne porte aucune information sinon le fait d'être là.
Il est bien sûr possible d'obtenir des distances LegoBlock arbitraires, par exemple en calculant des différences ou des rapports de distances.
negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0
threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50
crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37
Toutes les valeurs ci-dessus sont valides et se compilent, cependant on s'intéresse particulièrement aux valeurs comme fourStuds qui possèdent à la fois les propriétés nonFractional et nonNegative, car elles représentent des blocs physiques que l'on peut combiner avec
combineLegoBlocks
: Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add
Ajoutons quelques fonctions pour permettre de créer et d'affiner des distances LegoBlock.
newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist
floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))
ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))
absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)
Note que floorDistance, ceilingDistance et absDistance peuvent gérer d'autres unités que LegoBlock et, d'une manière générale, ne font aucune supposition sur les propriétés en entrée : elles garantissent seulement que le résultat aura une propriété précise, nonFractional ou nonNegative.
Regardons quelques résultats.
import Distance exposing (Distance)
-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds
-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud
-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)
-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud
-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)
-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud
-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))
-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))
D'une manière générale, floorDistance et ceilingDistance donnent des résultats différents, mais offrent les mêmes garanties.
C'est toute la force de la technique du type fantôme : offrir des choix souples aux utilisateurs tout en conservant de solides garanties.
Toi, le Maître du Mal, tu es très fier de la qualité des TreasureChests que l'on trouve dans tes donjons maléfiques aux quatre coins du monde.
Ce n'est pas facile de maintenir cette qualité, tes gestionnaires de donjon n'arrêtent pas de rater la fabrication des trésors, alors tu décides de fournir une API Elm pour les plier à ta volonté.
Il y a deux conditions sur lesquelles tu ne céderas pas :
TreasureChest doit être protégé par un mot de passe sécurisé d'au moins 8 caractèresTreasureChest doit contenir un trésor uniqueTes gestionnaires de donjon te proposeront une liste de suggestions de mots de passe et de trésors, et seuls les TreasureChests adéquats seront créés.
Avec ces critères, il y a deux façons possibles de créer des coffres sécurisés à partir d'une liste de suggestions :
Ces deux approches ne donneront pas forcément les mêmes résultats (par exemple pour [("strong_password", GoldStatue), ("1234", GoldStatue)]), mais peu t'importe, tu veux laisser les gestionnaires de donjon décider.
Une API qui laisse le choix aux utilisateurs, tout en garantissant les propriétés du résultat final ? On dirait le cas d'usage parfait pour la technique du type fantôme !
Le type TreasureChest et son compagnon getTreasure sont déjà fournis, mais tu dois concevoir un type pour une suggestion de coffre.
Implémente le type Chest, implémente makeChest et corrige les signatures de type de secureChest et uniqueTreasures.
Les signatures de type de makeChest et makeTreasureChest sont déjà fournies dans l'énoncé, ne les modifie pas.
Un Chest doit contenir les mêmes données qu'un TreasureChest et doit prendre deux arguments de type, un pour treasure et un type fantôme d'enregistrement pour conditions.
Note que, comme Chest utilise un type fantôme, il doit donc être opaque pour empêcher qu'on l'utilise en dehors du module TreasureFactory.
Dans ce cas, il n'est même pas exposé du tout.
Modifie les signatures de type de secureChest et uniqueTreasures pour ajouter les contraintes en utilisant des enregistrements extensibles comme types fantômes.
secureChest doit prendre un Chest sans conditions spécifiques et renvoyer un Maybe Chest, avec la condition supplémentaire securePassword : () dans son enregistrement fantôme.
uniqueTreasures doit prendre une List Chest sans conditions spécifiques et renvoyer une List Chest, avec la condition supplémentaire uniqueTreasure : () ajoutée à l'enregistrement fantôme.
Les tests Elm ont accès aux fonctions exposées, mais pas aux signatures de type ; il est donc impossible pour les tests de vérifier que tu utilises les bonnes signatures. Bien sûr, la meilleure indication que tes signatures sont les bonnes, c'est de réussir à compiler et exécuter le module et les tests, mais nous avons créé une règle d'analyseur qui vérifiera tes signatures de type une fois que tu auras soumis ta solution.
Une fois que tes coffres sont prêts, il te faut choisir ceux qui sont sécurisés.
Implémente secureChest, qui ne renvoie une variante Just que pour les coffres dont le mot de passe fait 8 caractères ou plus.
Les Chest qui remplissent cette condition doivent avoir une condition supplémentaire securePassword : () ajoutée à leur type fantôme.
Seuls les trésors les plus rares devraient avoir leur place dans un donjon : une seule copie suffit à donner l'impression qu'un trésor est bon marché.
Implémente uniqueTreasures, qui prend une liste de Chest et renvoie une liste des Chest dont le trésor est unique dans la liste d'entrée.
Si un trésor apparaît deux fois dans l'entrée, il ne doit pas se retrouver dans la sortie.
Les Chest d'entrée ne doivent pas avoir de conditions spécifiques, et ceux de sortie doivent avoir la condition supplémentaire uniqueTreasure : () ajoutée.
Savoure les meilleurs TreasureChests, qui attireront à coup sûr les aventuriers comme le miel attire les mouches.
Implémente makeTreasureChest, qui prend un Chest à la fois sécurisé et unique et crée un TreasureChest.
Comme TreasureChest est un type opaque, ce sera le seul moyen d'en créer un ; même les piètres gestionnaires de donjon ne pourront pas tout gâcher.
Inscris-toi sur Exercism pour apprendre et maîtriser Elm avec 28 concepts110 exercices, et un vrai mentorat humain, le tout gratuitement.