Type Option

Présentation

A.k.a Maybe (Haskell), Optional (Java 8)

Modélise l'absence de valeur → Dans le sens d'une possibilité pour une valeur d'être absente → Différent de unit qui est utilisé dans le cas où il n'y a jamais de valeur

Défini sous la forme d'une union avec 2 cases :

type Option<'Value> =
    | None              // Case sans donnée → quand valeur absente
    | Some of 'Value    // Case avec donnée → quand valeur présente

Cas d'utilisation

Modéliser un champ optionnel

type Civility = Mr | Mrs
type User = { Name: string; Civility: Civility option }

let joey  = { Name = "Joey"; Civility = Some Mr }
let guest = { Name = "Guest"; Civility = None }

→ Rend explicite le fait que Name est obligatoire et Civility facultatif

Attention : ce design n'empêche pas ici d'avoir Name = null (limite BCL)

Opération partielle

Opération où aucune valeur de sortie n'est possible pour certaines entrées.

Exemples

Exemple 1 - Inverse d'un nombre

let inverse n = 1.0 / n

let tryInverse n =
    match n with
    | 0.0 -> None
    | n   -> Some (1.0 / n)

Fonction

Opération

Signature

n = 0.5

n = 0.0

inverse

Partielle

float -> float

2.0

infinity

tryInverse

Totale

float -> float option

Some 2.0

None 👌

Exemple 2 - Recherche d'un élément dans une collection

  • Opération partielle : find predicate → 💥 quand élément non trouvé

  • Opération totale : tryFind predicateNone ou Some item

Avantages 👍

  • Explicite (honnête) concernant la partialité de l'opération

    • Pas de valeur spéciale (et cachée) : null, infinity

    • Pas d'exception

  • Force le code appelant à gérer la totalité des cas :

    • Présence d'une valeur en sortie : Some value

    • Absence d'une valeur en sortie : None

Flux de contrôle

Pour tester la présence de la valeur (de type 'T) dans l'option

  • ❌ Ne pas utiliser IsSome, IsNone et Value (🤞💥)

    • if option.IsSome then option.Value...

  • 👌 A la main avec pattern matching

  • ✅ Fonctions du module Option

Manuel avec pattern matching

Exemple :

let print option =
    match option with
    | Some x -> printfn "%A" x
    | None   -> printfn "None"

print (Some 1.0)  // 1.0
print None        // None

Intégré au module Option

Opération de Mapping de la valeur (de type 'T) si ∃ :

  • option |> Option.map f avec f opération totale 'T -> 'U

  • option |> Option.bind f avec f opération partielle 'T -> 'U option

Conserver la valeur si ∃ et si respecte condition :

  • option |> Option.filter predicate avec predicate: 'T -> bool appelé que si valeur ∃

Exercice

Implémenter map, bind et filter avec pattern matching

Solution

let map f option =             // (f: 'T -> 'U) -> 'T option -> 'U option
    match option with
    | Some x -> Some (f x)
    | None   -> None           // 🎁 1. Pourquoi on ne peut pas écrire `None -> option` ?

let bind f option =            // (f: 'T -> 'U option) -> 'T option -> 'U option
    match option with
    | Some x -> f x
    | None   -> None

let filter predicate option =  // (predicate: 'T -> bool) -> 'T option -> 'T option
    match option with
    | Some x when predicate x -> option
    | _ -> None                // 🎁 2. Implémenter `filter` avec `bind` ?

Réponses 🎁

// 🎁 1. Pourquoi on ne peut pas écrire `None -> option` :
let map (f: 'T -> 'U) (option: 'T option) : 'U option =
    match option with
    | Some x -> Some (f x)
    | None   -> (*None*) option  // 💥 Erreur de typage : `'U option` attendu != `'T option`
// 🎁 2. Implémenter `filter` avec `bind` :
let filter predicate option =  // (predicate: 'T -> bool) -> 'T option -> 'T option
    let f x = if predicate x then option else None
    bind f option

Exemple

// Application console de questions/réponses
type Answer = A | B | C | D

let tryParseAnswer text =
    match text with
    | "A" -> Some A
    | "B" -> Some B
    | "C" -> Some C
    | "D" -> Some D
    | _   -> None

// Fonction appelée quand l'utilisateur saisit la réponse au clavier à la question posée
let checkAnswer (expectedAnswer: Answer) (givenAnswer: string) =
    tryParseAnswer givenAnswer
    |> Option.filter ((=) expectedAnswer)
    |> Option.map (fun _ -> "✅")
    |> Option.defaultValue "❌"

["X"; "A"; "B"] |> List.map (checkAnswer B)  // ["❌"; "❌"; "✅"]

Bénéfices

Rend logique métier + lisible

  • Pas de if hasValue then / else

  • Met en valeur le happy path

  • Centralise à la fin la gestion de l'absence de valeur

💡 Les 🚀 Computation expression (CE)📍 fournissent une syntaxe alternative + légère

Option vs List

Option ≃ Liste de 0 ou 1 élément → cf. fonction Option.toList

let noneIsEmptyList       = Option.toList(None)   = []   // true
let someIsListWithOneItem = Option.toList(Some 1) = [1]  // true

☝ Une List peut avoir + de 1 élément → Type Option modélise mieux l'absence de valeur que type List

💡 Module Option : beaucoup de même fonctions que module Listcontains, count, exist, filter, fold, forall, map

Option vs null

De part ses interactions avec la BCL, F♯ autorise parfois la valeur null

👉 Bonne pratique → Isoler ces cas de figure et wrapper dans un type Option → Par exemple en utilisant la fonction Option.ofObj

let readLine (reader: System.IO.TextReader) =
    reader.ReadLine() |> Option.ofObj

    // Équivalent à faire :
    match reader.ReadLine() with
    | null -> None
    | line -> Some line

Option vs Nullable

Type System.Nullable<'T>Option<'T> en + limité

  • ❗ Ne marche pas pour les types références

  • ❗ Manque comportement monadique i.e. fonctions map et bind

  • ❗ En F♯, pas de magie comme en C♯ / mot clé null

👉 Option est le type idiomatique en F♯

💡 On utilise le type Nullable en cas d'interop. Pour l'instancier : → ❌ Contrairement au C♯, le mot clé null ne marche pas ! → 👎 L'emploie de Unchecked.defaultof marche mais n'est pas élégant. → 👍 Utiliser l'un des constructeurs : Nullable() ou Nullable(value)

open System

let x: Nullable<int> = null
// ⚠️ The type 'Nullable<int>' does not have 'null' as a proper value. To create a null value for a Nullable type use 'System.Nullable()' (FS43)

let x = Unchecked.defaultof<Nullable<int>>
// val x: Nullable<int> = <null>

let x' = Nullable<int>()
// val x': Nullable<int> = <null>

let y = Nullable(1)
// val y: Nullable<int> = 1

Mis à jour

Ce contenu vous a-t-il été utile ?