Unions
A.k.a. Discriminated Unions (DU)
Points clés
Terme exacte : « Union discriminée », Discriminated Union (DU)
Types Somme : représente un OU, un choix entre plusieurs Cases
Même principe que pour une
enum
mais généralisé
Chaque case doit avoir un Tag (a.k.a Label)
C'est le discriminant de l'union pour identifier le case
Chaque case peut contenir des données
type Billet =
| Adulte // aucune donnée -> ≃ singleton stateless
| Senior of int // contient un 'int' (mais on ne sait pas ce que c'est)
| Enfant of age: int // contient un 'int' de nom 'age'
| Famille of Billet list // contient une liste de billet
// type récursif -- pas besoin de 'rec'
Qualification des Tags
Les Tags peuvent être utilisés :
sans qualification →
Adulte
sauf pour résoudre un conflit de noms ou par choix de nommage →
Billet.Adulte
☝ On peut forcer l'usage avec qualification en décorant l'union de l'attribut RequireQualifiedAccess
, essentiellement pour des raisons de nommage de l'union et de ses tags, pour que le code se lise sans ambiguïté.
Casse des Tags
Les Tags doivent être nommés en PascalCase ❗
💡 Depuis F# 7.0, la camelCase est possible si l'union est décorée avec RequireQualifiedAccess.
Champs nommés - Labelled Fields
Pratiques pour :
Ajouter un sens à un type primitif : → Dans l'exemple précédent, en ligne 4, le case
Enfant
contient un champ de typeint
qui est nomméage
.Distinguer deux champs du même type au sein d'un Tuple : → Exemple :
type ComplexNumber =
| Cartesian of Real: float * Imaginary: float
| Polar of Magnitude: float * Phase: float
☝ Notes :
Le nommage des champs est optionnel.
En tant que champ, on optera pour le PascalCase. Mais on peut aussi les voir en tant que paramètres, alors en camelCase.
Quand un case contient plusieurs champs, on peut n'en nommer que certains. → Mais je ne recommande pas cette dissymétrie.
Déclaration
Sur plusieurs lignes : 1 ligne / case → ☝ Ligne indentée et commençant par |
Sur une seule ligne -- si déclaration reste courte ❗ → 💡 Pas besoin du 1er |
open System
type IntOrBool =
| Int32 of Int32 // 💡 Tag de même nom que ses données
| Boolean of Boolean
type OrderId = OrderId of int // 👈 Single-case union
// 💡 Tag de même nom que l'union parent
type Found<'T> = Found of 'T | NotFound // 💡 Type générique
Instanciation
Tag ≃ constructeur → Fonction appelée avec les éventuelles données du case
type Shape =
| Circle of radius: int
| Rectangle of width: int * height: int
let circle = Circle 12 // Type: 'Shape', Valeur: 'Circle 12'
let rect = Rectangle (4, 3) // Type: 'Shape', Valeur: 'Rectangle (4, 3)'
// Utilisation du nom des champs
let rec2 = Rectangle (height = 4, width = 6)
let circles = [1..4] |> List.map Circle // 👈 Tag employé comme fonction
Conflit de noms
Quand 2 unions ont des tags de même nom → Qualifier le tag avec le nom de l'union
type Shape =
| Circle of radius: int
| Rectangle of width: int * height: int
type Draw = Line | Circle // 'Circle' sera en conflit avec le tag de 'Shape'
let draw = Circle // Type='Draw' (type le + proche) -- ⚠️ à éviter car ambigu
// Tags qualifiés par leur type union
let shape = Shape.Circle 12
let draw' = Draw.Circle
Accès aux données internes
Uniquement via pattern matching Matching d'un type Union est exhaustif
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let area shape =
match shape with
| Circle r -> Math.PI * r * r // 💡 Même syntaxe que instanciation
| Rectangle (w, h) -> w * h
let isFlat = function
| Circle 0. // 💡 Constant pattern
| Rectangle (0., _)
| Rectangle (_, 0.) -> true // 💡 OR pattern
| Circle _
| Rectangle _ -> false
Single-case union
Union avec un seul cas encapsulant un type (généralement primitif)
type CustomerId = CustomerId of int
type OrderId = OrderId of int
let fetchOrder (OrderId orderId) = // 💡 Déconstruction directe sans 'match'
...
Assure type safety contrairement au simple type alias → Impossible de passer un CustomerId
à une fonction attendant un OrderId
👍
Permet d'éviter Primitive Obsession à coût minime
Style "enum"
Tous les cases sont vides = dépourvus de données → ≠ enum
.NET 📍Enums
L'instanciation et le pattern matching se font juste avec le tag → Le tag n'est plus une fonction mais une valeur (singleton)
type Answer = Yes | No | Maybe
let answer = Yes
let print answer =
match answer with
| Yes -> printfn "Oui"
| No -> printfn "Non"
| Maybe -> printfn "Peut-être"
Last updated
Was this helpful?