🚀 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 contenirlet
,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 typeAsync<'X>
seq { yield x }
renvoie un typeSeq<'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
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 ⚠️
Verbeux : les
log x
gĂŞnent lectureError 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
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>>
Last updated
Was this helpful?