🚀 Computation expression (CE)

Computation expression

Sucre syntaxique cachant une « machinerie »

  • Applique la Separation of Concerns

  • Code + lisible Ă  l'intĂ©rieur de la computation expression (CE)

Syntaxe : builder { expr }

  • builder instance d'un Builder📍

  • expr peut contenir let, let!, do!, yield, yield!, return, return!

đź’ˇ Note : seq, async et task sont des CE

Builder

Une computation expression s'appuie sur un objet appelé Builder. → Cet objet permet éventuellement de stocker un état en background.

Pour chaque mot-clé supporté (let!, return...), le Builder implémente une ou plusieurs méthodes associées. Exemples :

  • builder { return expr } → builder.Return(expr)

  • builder { let! x = expr; cexpr } → builder.Bind(expr, (fun x -> {| cexpr |}))

Le builder peut également wrappé le résultat dans un type qui lui est propre :

  • async { return x } renvoie un type Async<'X>

  • seq { yield x } renvoie un type Seq<'X>

Builder desugaring

Le compilateur opère la traduction vers les méthodes du builder.

→ La CE masque la complexité de ces appels, souvent imbriqués :

seq {
    for n in list do
        yield n
        yield n * 10 }

// Traduit en :
seq.For(list, fun () ->
    seq.Combine(seq.Yield(n),
                seq.Delay(fun () -> seq.Yield(n * 10)) ) )

Exemples

logger

Besoin : logguer les valeurs intermédiaires d'un calcul

let log value = printfn $"{value}"

let loggedCalc =
    let x = 42
    log x  // âť¶
    let y = 43
    log y  // âť¶
    let z = x + y
    log z  // âť¶
    z

Problèmes ⚠️

  1. Verbeux : les log x gĂŞnent lecture

  2. Error prone : oublier un log, logguer mauvaise valeur...

đź’ˇ Rendre les logs implicites dans une CE lors du let! / Bind :

type LoggingBuilder() =
    let log value = printfn $"{value}"; value
    member _.Bind(x, f) = x |> log |> f
    member _.Return(x) = x

let logger = LoggingBuilder()

//---

let loggedCalc = logger {
    let! x = 42
    let! y = 43
    let! z = x + y
    return z
}

maybe

Besoin : simplifier enchaînement de "trySomething" renvoyant une Option

let tryDivideBy bottom top = // (bottom: int) -> (top: int) -> int option
    if (bottom = 0) or (top % bottom <> 0)
    then None
    else Some (top / bottom)

// Sans CE
let division =
    36
    |> tryDivideBy 2                // Some 18
    |> Option.bind (tryDivideBy 3)  // Some 6
    |> Option.bind (tryDivideBy 2)  // Some 3

// Avec CE
type MaybeBuilder() =
    member _.Bind(x, f) = x |> Option.bind f
    member _.Return(x) = Some x

let maybe = MaybeBuilder()

let division' = maybe {
    let! v1 = 36 |> tryDivideBy 2
    let! v2 = v1 |> tryDivideBy 3
    let! v3 = v2 |> tryDivideBy 2
    return v3
}

Bilan : ✅ Symétrie, ❌ Valeurs intermédiaires

Limites

Imbrication de CE

✅ On peut imbriquer des CE différentes ❌ Mais code devient difficile à comprendre

Exemple : combiner logger et maybe âť“

Solution alternative :

let inline (>>=) x f = x |> Option.bind f

let logM value = printfn $"{value}"; Some value  // 'a -> 'a option

let division' =
    36 |> tryDivideBy 2 >>= logM
      >>= tryDivideBy 3 >>= logM
      >>= tryDivideBy 2 >>= logM

Combinaison de CE

Combiner Async + Option/Result ? → Solution : CE asyncResult + helpers dans FsToolkit

type LoginError =
    | InvalidUser | InvalidPassword
    | Unauthorized of AuthError | TokenErr of TokenError

let login username password =
    asyncResult {
        // tryGetUser: string -> Async<User option>
        let! user = username |> tryGetUser |> AsyncResult.requireSome InvalidUser
        // isPasswordValid: string -> User -> bool
        do! user |> isPasswordValid password |> Result.requireTrue InvalidPassword
        // authorize: User -> Async<Result<unit, AuthError>>
        do! user |> authorize |> AsyncResult.mapError Unauthorized
        // createAuthToken: User -> Result<AuthToken, TokenError>
        return! user |> createAuthToken |> Result.mapError TokenErr
    } // Async<Result<AuthToken, LoginError>>

Mis Ă  jour

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