Type Result

Présentation

A.k.a Either (Haskell)

Modélise une double-track Succès/Échec

type Result<'Success, 'Error> =   // ☝ 2 paramètres génériques
    | Ok of 'Success    // Track "Succès" == Happy path
    | Error of 'Error   // Track "Échec"  == Voie de garage

Gestion fonctionnelle des erreurs métier (les erreurs prévisibles)

  • Permet de limiter usage des exceptions aux erreurs exceptionnelles

  • Dès qu'une opération échoue, les opérations restantes ne sont pas lancées

  • Railway-oriented programming, F# for Fun and Profit

Module Result

Ne contient que 3 fonctions (en F# 5.0) :

map f option : sert à mapper le résultat

  • ('T -> 'U) -> Result<'T, 'Error> -> Result<'U, 'Error>

mapError f option : sert à mapper l'erreur

  • ('Err1 -> 'Err2) -> Result<'T, 'Err1> -> Result<'T, 'Err2>

bind f option : idem map avec fonction f qui renvoie un Result

  • ('T -> Result<'U, 'Error>) -> Result<'T, 'Error> -> Result<'U, 'Error>

  • 💡 Le résultat est aplati, comme la fonction flatMap sur les arrays JS

  • ⚠️ Même type d'erreur 'Error pour f et le result en entrée

💡 Depuis F# 7.0, le module Result contient plus de fonctions afin d'être aussi fonctionnel que le type Option , avec les correspondances suivantes :

Option
Result
List

Some value

Ok value

[ value ]

None

Error () / Error _

[ ]

isSome / isNone

isOk / isError

× / isEmpty

contains someValue

contains okValue

contains value

count

count

length

defaultValue value defaultWith defThunk

defaultValue value defaultWith defThunk

exists predicate forall predicate

exists predicate forall predicate

exists predicate forall predicate

filter predicate

×

filter predicate

iter action map f / bind f fold f state

iter action map f / bind f fold f state

iter action map f / bind f fold f state

×

mapError f

×

toArray / toList

toArray / toList

×

×

toOption

×

Quiz 🎲

Implémenter Result.map et Result.bind

💡 Tips :

  • Mapping sur la track Succès

  • Accès à la valeur dans la track Succès :

    • Utiliser pattern matching (match result with...)

  • Retour : simple Result, pas un Result<Result> !

Solution

// ('T -> 'U) -> Result<'T, 'Error> -> Result<'U, 'Error>
let map f result =
    match result with
    | Ok x    -> Ok (f x)  // ☝ Ok -> Ok
    | Error e -> Error e   // ⚠️ Les 2 `Error e` n'ont pas le même type !

// ('T -> Result<'U, 'Error>) -> Result<'T, 'Error>
//                            -> Result<'U, 'Error>
let bind f result =
    match result with
    | Ok x    -> f x       // ☝ Ok -> Ok ou Error !
    | Error e -> Error e

Result : tracks Success/Failure

map : pas de changement de track

Track      Input          Operation      Output
Success ─ Ok x    ───► map( x -> y ) ───► Ok y
Failure ─ Error e ───► map(  ....  ) ───► Error e

bind : routage possible vers track Failure mais jamais l'inverse

Track     Input              Operation           Output
Success ─ Ok x    ─┬─► bind( x -> Ok y     ) ───► Ok y
                   └─► bind( x -> Error e2 ) ─┐
Failure ─ Error e ───► bind(     ....      ) ─┴─► Error ~

☝ Opération de mapping/binding jamais exécutée dans track Failure

Result vs Option

Option peut représenter le résultat d'une opération qui peut échouer ☝ Mais en cas d'échec, l'option ne contient pas l'erreur, juste None

Option<'T>Result<'T, unit>

  • Some xOk x

  • NoneError ()

  • Cf. fonctions Option.toResult et Option.toResultWith error de FSharpPlus

let toResultWith (error: 'Error) (option: 'T option) : Result<'T, 'Error> =
    match option with
    | Some x -> Ok x
    | None   -> Error error

Exemple :

Modification de la fonction checkAnswer précédente pour indiquer l'erreur :

open FSharpPlus

type Answer = A | B | C | D
type Error = InvalidInput | WrongAnswer

let tryParseAnswer text = ... // string -> Answer option

let checkAnswer (expectedAnswer: Answer) (givenAnswer: string) =
    let check answer = if answer = expectedAnswer then Ok answer else Error WrongAnswer
    tryParseAnswer givenAnswer           // Answer option
    |> Option.toResultWith InvalidInput  // Result<Answer, Error>
    |> Result.bind check
    |> function
       | Ok _               -> "✅"
       | Error InvalidInput -> "❌ Invalid Input"
       | Error WrongAnswer  -> "❌ Wrong Answer"

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

Result vs Validation

Result est "monadique" : à la 1ère erreur, on "débranche"

Validation est "applicatif" : permet d'accumuler les erreurs

Plus d'info : 🔗 Validation with F# 5 and FsToolkit, Compositional IT, Dec 2020

Mis à jour

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