There are a few things I would probably add or change about your implementation. When I create a new computation builder, I typically put the logic in a module, and then just call the functions in that module from the builder.
Another thing I would do is prefer the use of discriminated unions for Error types. This makes it possible to match on the error, and also lets you use a little trick with the implementation of delay/run on the computation builder to wrap up any unhandled exceptions that occur inside your computation expression as an Error as well, so you don't have to wrap things in try/with.
Next, I would implement some common operators like >>=
, <*>
, and <!>
, as well as lift
and apply
functions. These will make it easier to work with the Results outside of the computation expression and interface with functions that don't natively work with Results.
Finally, I would implement at least all the standard members for the computation builder, including TryWith/TryFinally/Using/For. An example implementation might look like the following:
module Either =
open FSharp.Reflection
open System
let inline bind f x =
match x with
| Ok s -> f s
| Error e -> Error e
let inline ok x = Ok x
let inline zero () = Ok ()
let inline convertExceptionToResult<'result,'event> (ex: exn) =
let union = FSharpType.GetUnionCases(typeof<Result<'result, 'event>>)
if typeof<'event>.IsAssignableFrom(typeof<exn>)
then FSharpValue.MakeUnion(union.[1], [|[ex] |> box|]) |> unbox<Result<'result, 'event>>
elif FSharpType.IsUnion typeof<'event>
then let cases = FSharpType.GetUnionCases(typeof<'event>)
match cases |> Seq.tryFind (fun case -> case.GetFields().Length = 1 && case.GetFields().[0].PropertyType.IsAssignableFrom(typeof<exn>)) with
| Some case -> FSharpValue.MakeUnion(union.[1], [|[FSharpValue.MakeUnion(case, [|ex |> box|]) |> unbox<'event>] |> box|]) |> unbox<Result<'result, 'event>>
| None -> failwithf "No Union Case of Event Type %s Supports Construction from an Unhandled Exception: \r\n%O" typeof<'event>.Name ex
else failwithf "Unable To Construct a Failure of type %s from Unhandled Exception: \r\n%O" typeof<'event>.Name ex
let inline delay<'result,'event> (f: unit -> Result<'result, 'event>) = fun () ->
try f()
with | ex -> convertExceptionToResult ex
let inline run<'result,'event> (f: unit -> Result<'result, 'event>) =
try f()
with | ex -> convertExceptionToResult ex
let inline (>>=) result f = bind f result
let inline apply wrappedFunction result =
match wrappedFunction, result with
| Ok a, Ok b -> Ok (a b)
| Error e, Ok s -> Error e
| Ok s, Error e -> Error e
| Error e, Error _ -> Error e
let inline (<*>) wrappedFunction result = apply wrappedFunction result
let inline lift f result = apply (ok f) result
let inline (<!>) f result = lift f result
type EitherBuilder() =
member __.Zero() = zero()
member __.Bind(x, f) = bind f x
member __.Return(x) = ok x
member __.ReturnFrom(x) = x
member __.Yield(x) = ok x
member __.YieldFrom(x) = x
member __.Combine (a, b) = bind b a
member __.Delay f = delay f
member __.Run f = run f
member __.TryWith (body, handler) =
try body()
with | ex -> handler ex
member __.TryFinally (body, compensation) =
try body()
finally compensation()
member this.Using(d:#IDisposable, body) =
let result = fun () -> body d
this.TryFinally (result, fun () ->
match d with
| null -> ()
| d -> d.Dispose())
member this.While (guard, body) =
if not <| guard ()
then this.Zero()
else bind (fun () -> this.While(guard, body)) (body())
member this.For(s:seq<_>, body) =
this.Using(s.GetEnumerator(), fun enum ->
this.While(enum.MoveNext,
this.Delay(fun () -> body enum.Current)))
let either = EitherBuilder()
Looking at your example usage of the EitherBuilder, I would probably change the while loop to a tail-recursive function. You can still use while loops with the new EitherBuilder if you want, but using a tail-recursive function allows you to eliminate the mutable state, and correctly defining your Error cases as a discriminated union will allow you to access the failing values if you need to without having the global state i
. That would look something like this:
open Either
type Errors =
| OutOfRange of int
| UnhandledException of exn // This is required to allow conversion from Exceptions to Errors
let func i =
if i < 3 then Ok(i)
else Error <| OutOfRange i
let result =
let rec loop i res =
either {
if i < 10
then let! output = func i
return! loop (i + 1) output
else return res
}
loop 0 0
In which case, result
has the value:
Result<int,Errors> = Error (OutOfRange 3)