Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?

Railway Oriented Programming and F# Result, part 2

Part 1: https://gist.github.com/mrange/1d2f3a26ca039588726fd3bd43cc8df3

Full source: https://gist.github.com/mrange/67553b312bd6a952690defe4bce3b126

In part 1 I briefly described RResult<_> that I find sometimes is better suited for ROP than Option<_> (because RResult<_> allows capture of error information) or Result<_,_> (because RResult<_> has a homogeneous error type):

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

A problem with RResult<_> compared to Option<_> is that there is no nice way to represent an empty value like None. However, it is rather easy to expand RResult<_> to support empty values:

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

rreturn and rbind are easy to implement:

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

When we like to combine two RResult<_> using for example rpair the question arise how we should combine Empty and Bad result. For rpair I believe it makes sense to return the Bad result as this is the most serious problem that caused rpair to fail.

let rpair (u : RResult<'U>) (t : RResult<'T>) : RResult<'T*'U> =
  // Note that: Empty && Bad ==> Bad
  match t, u with
  | RResult.Good    tgood , RResult.Good  ugood -> rreturn (tgood, ugood)
  | RResult.Bad     tbad  , RResult.Bad   ubad  -> RResult.Bad (tbad.Join ubad)
  | _                     , RResult.Bad   ubad  -> RResult.Bad ubad
  | RResult.Bad     tbad  , _                   -> RResult.Bad tbad
  | _                     , _                   -> RResult.Empty

However, for rorElse it I think it makes sense to return the "least faulty value" as that is the purpose of rorElse. That means if we combine an Empty and Bad result we should return Empty.

let rorElse (s : RResult<'T>) (f : RResult<'T>) : RResult<'T> =
  // Note that: Empty || Bad ==> Empty
  match f, s with
  | RResult.Good      _   , _                     -> f
  | _                     , RResult.Good    _     -> s
  | RResult.Empty         , _                     -> RResult.Empty
  | _                     , RResult.Empty         -> RResult.Empty
  | RResult.Bad       fbad, RResult.Bad     sbad  -> RResult.Bad (fbad.Join sbad)

The API for RResult<_> is mostly unchanged except that pattern matching on RResult<_> must include an Empty case.

Wrapping up

My initial attempt at work with RResult<_> was basically Result<_,_> with a fixed error type. Over time the need to represent an empty good value grew and I found it useful to extend RResult<_> with an Empty value.

This has allowed me to combine functions that has a tristate outcome using ROP in a clean and a succinct manner.

I hope this was interesting.

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