Create a gist now

Instantly share code, notes, and snippets.

Railway Oriented Programming and F# Result

Railway Oriented Programming and F# Result

Full source: https://gist.github.com/mrange/aa9e0898492b6d384dd839bc4a2f96a1

Option<_> is great for ROP (Railway Oriented Programming) but we get no info on what went wrong (the failure value is None which carries no info).

With the introduction F# 4.1 we got Result<_, _> a "smarter" Option<_> as it allows us to pass a failure value.

However, when one inspects the signature of Result.bind one sees a potential issue for ROP:

val bind: binder:('T -> Result<'U, 'TError>) -> result:Result<'T, 'TError> -> Result<'U, 'TError>

bind requires the same 'TError for both result and binder.

This means that composing two functions like this is difficult:

let f () : Result<int, string>  = Ok 1
let g i  : Result<int, exn>     = Ok 1

let x = f () |> Result.bind g // Doesn't compile as string doesn't match exn for 'TError

Result allows us to map errors using Result.mapError allowing us to overcome this issue by mapping exn to string:

let y = f () |> Result.bind (g >> Result.mapError (fun e -> e.Message))

Or let's say we cast 'TError to obj always:

let toObj v = v :> obj
let z = f () |> Result.mapError toObj |> Result.bind (g >> Result.mapError toObj)

Or more succinct:

let rerrorToObj t = Result.mapError (fun v -> v :> obj) t
let z = f () |> rerrorToObj |> Result.bind (g >> rerrorToObj)

It feels a bit clunky and if we always cast all error objects to obj perhaps 'TError should always be obj?

let f () : Result<int, obj> = Ok 1
let g i  : Result<int, obj> = Ok 1

let x = f () |> Result.bind g // Compiles fine now

Finding a homogeneous error type

This is a bit how exceptions in .NET works. We combine objects of heterogeneous types but all exceptions inherits a common base class exn.

Exceptions and ROP wants to solve the same problem: "How can we make sure that happy path code isn't hidden by the error-handling for all unhappy paths".

It's not unreasonable to think that to enable ROP we need a homogeneous error type. I would suggest something other than obj though.

One suggestion is this:

[<RequireQualifiedAccess>]
type RBad =
  | Message         of string
  | Exception       of exn
  | Object          of obj
  | DescribedObject of string*obj

This allows us to pass messages, exceptions but also all kinds of objects that we didn't foresee as errors. For tracing we also allow describing an error object.

However, when implementing result combinators one realizes that there is a need to combine errors as well.

Consider the common functional pattern Applicative:

let r =
  Ok someFunction
  <*> argument1
  <*> argument2
  <*> argument3
  <*> argument4

Applicative will apply argument 1 to 4 to someFunction if and only if all arguments are Ok. Otherwise it returns an Error. It can be sensible that Error contains all argument errors, not just the first one.

In addition there is sometimes the need to pair results:

let p = rpair (x, y)

But rpair should also pair the error results if any.

In order to support that the error type could look like this:

[<RequireQualifiedAccess>]
type RBadTree =
  | Leaf  of RBad
  | Fork  of RBadTree*RBadTree

RResult<_>

We are ready to define our result type:

type RResult<'T> = Result<'T, RBadTree>

But as type abbreviations has limitations I instead will define RResult<_> as this:

[<RequireQualifiedAccess>]
[<Struct>]
type RResult<'T> =
  | Good  of good : 'T
  | Bad   of bad  : RBadTree

It's easy to define common functions for this type:

  let inline rreturn  v = RResult.Good v
  let inline rbind (uf : 'T -> RResult<'U>) (t : RResult<'T>) : RResult<'U> =
    match t with
    | RResult.Bad   tbad  -> RResult.Bad tbad
    | RResult.Good  tgood -> uf tgood

  // Kleisli

  let inline rarr f         = fun v -> rreturn (f v)
  let inline rkleisli uf tf = fun v -> rbind uf (tf v)

  // Applicative

  let inline rpure  f = rreturn f
  let inline rapply (t : RResult<'T>)  (f : RResult<'T -> 'U>) : RResult<'U> =
    match f, t with
    | RResult.Bad   fbad  , RResult.Bad   tbad  -> RResult.Bad (fbad.Join tbad)
    | RResult.Bad   fbad  , _                   -> RResult.Bad fbad
    | _                   , RResult.Bad   tbad  -> RResult.Bad tbad
    | RResult.Good  fgood , RResult.Good  tgood -> rreturn (fgood tgood)

  // Functor

  let inline rmap (m : 'T -> 'U) (t : RResult<'T>) : RResult<'U> =
    match t with
    | RResult.Bad   tbad  -> RResult.Bad tbad
    | RResult.Good  tgood -> rreturn (m tgood)

  // Lifts

  let inline rgood    v   = rreturn v
  let inline rbad     b   = RResult.Bad (RBadTree.Leaf b)
  let inline rmsg     msg = rbad (RBad.Message msg)
  let inline rexn     e   = rbad (RBad.Exception e)

As this is a type, not a type abbreviation, we can also extend it with common operators:

type RResult<'T> with
  static member inline (>>=)  (x, uf) = RResult.rbind    uf x
  static member inline (<*>)  (x, t)  = RResult.rapply    t x
  static member inline (|>>)  (x, m)  = RResult.rmap      m x
  static member inline (<|>)  (x, s)  = RResult.rorElse   s x
  static member inline (~%%)  x       = RResult.rderef    x
  static member inline (%%)   (x, bf) = RResult.rderefOr bf x

Note that rapply joins the bad results which can be seen in this example:

rgood (fun x y z -> x + y + z)
  |> rapply (rgood 1        )
  |> rapply (rmsg  "Bad"    )
  |> rapply (rmsg  "Result" )
  |> printfn "%A"

Or more succinct using the rapply operator <*>:

rgood (fun x y z -> x + y + z)
  <*> rgood 1
  <*> rmsg  "Bad"
  <*> rmsg  "Result"
  |> printfn "%A"

This prints:

Bad (Fork (Leaf (Message "Bad"),Leaf (Message "Result")))

In Scott Wlaschin amazing ROP presentation he presents the an example on a workflow that needs error handling:

receiveRequest
>> validateRequest
>> canonicalizeEmail
>> updateDbFromRequest
>> sendEmail

Scott claims that with ROP the code with error handling and without error handling can be made to look the same. How does his example look with RResult<_>?

fun uri ->
  receiveRequest uri
  >>= validateRequest
  >>= canonicalizeEmail
  >>= updateDbFromRequest
  >>= sendEmail

Pretty good but can get even neater if one define the kleisli operator >=>:

receiveRequest
>=> validateRequest
>=> canonicalizeEmail
>=> updateDbFromRequest
>=> sendEmail

Wrapping up

At my work at Atomize AB I am been collecting information from various sources and combining them into results.

I found ROP to be a very useful pattern but Option<_> doesn't work for me as I need to pass not only the result but also what went wrong if there was an error.

I found Result<_, _> difficult to compose because the 'TError type might be incompatible. In addition, I was lacking a way to aggregate multiple errors into a homogeneous result.

That's why I created something that look very much like RResult<_> and that has proven itself useful as it's homogeneous error result simplifies ROP as well as it's ability to join error results is useful in my use case.

I hope this was interesting.

@kspeakman
kspeakman commented Mar 20, 2017 edited

This for the article. It is interesting.

There's basically 2 categories of errors here. The IO-based errors (receive request, update DB, and send email) at the outer layer which generally stop after the first one. And the validation errors which can return multiple errors and be combined. Rather than using a leaf/branch tree structure, have you considered just making a specific union type for validation errors and adding them as a case for the outer layer? E.g.

    type ValidationError =
    | UseCaseErrors of UseCaseError list // replace UseCase with operation
    | ...

    type OuterError =
    | ValidationFailed of ValidationError
    | ReceiveFailed of exn
    | UpdateFailed of exn
    | EmailSendFailed of exn

Then, the multiple-errors code can convert their errors up to ValidationFailed before returning to the outer layer.

I work mostly with APIs, so my version is ValidationFailed of obj list which I convert to JSON. It's more explicit to use an typed object, but since I'm in the necessarily-effectful outer edge of the system when the response is generated, I was ok with serializing obj to JSON.

@mrange
Owner
mrange commented Mar 22, 2017

Thanks for your feedback.

The purpose of the "outer error" is to be able to join multiple error.

Let's say you have pair function that combines the result of two RResult into a single RResult. The signature of pair is val pair: RResult<'T> -> RResult<'U> - RResult<'T*'U>. If both inputs are in the error state I like to combine them which I do through a Fork.

In your use case AFAICT there's no clean way to join multiple EmailSendFailed errors. If that is a problem or not depends on your use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment