F# Either computation expression with while loop
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
7
down vote
favorite
I want to implement an Either computation expression in F#. The computation executes a function inside a while
loop, and this function's return type is Result<int, string>
. The function might return an Error<string>
if some condition is met, and then the whole computation expression should be stopped.
This is the code I have so far to illustrate what I am trying to achieve:
type EitherBuilder () =
member this.Bind(x, f) =
match x with
| Ok s -> f s
| Error f -> Error f
member this.Return x = Ok(x)
member this.ReturnFrom x = x
member this.Zero() = Ok()
member this.Delay(f) = f
member this.Combine(x, f) = this.Bind(x, f)
member this.Run(f) = f()
member this.While(guard, body) =
if guard () then this.Bind(body (), fun _ -> this.While(guard, body))
else this.Zero()
let either = EitherBuilder ()
let mutable i = 0
let func i =
if i < 3 then Ok(i)
else Error("error")
let result = either
let mutable res = 0
while i < 10 do
let! output = func i
res <- output
i <- i + 1
return res
I have tested it and it looks like it is working and doing what I am after (in the example above, the computation is stopped when i
becomes 3 and result
is set to Error "error"
), but I am not 100% sure if the implementation of the EitherBuilder
is fully correct. I would appreciate some feedback.
functional-programming f# monads
add a comment |Â
up vote
7
down vote
favorite
I want to implement an Either computation expression in F#. The computation executes a function inside a while
loop, and this function's return type is Result<int, string>
. The function might return an Error<string>
if some condition is met, and then the whole computation expression should be stopped.
This is the code I have so far to illustrate what I am trying to achieve:
type EitherBuilder () =
member this.Bind(x, f) =
match x with
| Ok s -> f s
| Error f -> Error f
member this.Return x = Ok(x)
member this.ReturnFrom x = x
member this.Zero() = Ok()
member this.Delay(f) = f
member this.Combine(x, f) = this.Bind(x, f)
member this.Run(f) = f()
member this.While(guard, body) =
if guard () then this.Bind(body (), fun _ -> this.While(guard, body))
else this.Zero()
let either = EitherBuilder ()
let mutable i = 0
let func i =
if i < 3 then Ok(i)
else Error("error")
let result = either
let mutable res = 0
while i < 10 do
let! output = func i
res <- output
i <- i + 1
return res
I have tested it and it looks like it is working and doing what I am after (in the example above, the computation is stopped when i
becomes 3 and result
is set to Error "error"
), but I am not 100% sure if the implementation of the EitherBuilder
is fully correct. I would appreciate some feedback.
functional-programming f# monads
add a comment |Â
up vote
7
down vote
favorite
up vote
7
down vote
favorite
I want to implement an Either computation expression in F#. The computation executes a function inside a while
loop, and this function's return type is Result<int, string>
. The function might return an Error<string>
if some condition is met, and then the whole computation expression should be stopped.
This is the code I have so far to illustrate what I am trying to achieve:
type EitherBuilder () =
member this.Bind(x, f) =
match x with
| Ok s -> f s
| Error f -> Error f
member this.Return x = Ok(x)
member this.ReturnFrom x = x
member this.Zero() = Ok()
member this.Delay(f) = f
member this.Combine(x, f) = this.Bind(x, f)
member this.Run(f) = f()
member this.While(guard, body) =
if guard () then this.Bind(body (), fun _ -> this.While(guard, body))
else this.Zero()
let either = EitherBuilder ()
let mutable i = 0
let func i =
if i < 3 then Ok(i)
else Error("error")
let result = either
let mutable res = 0
while i < 10 do
let! output = func i
res <- output
i <- i + 1
return res
I have tested it and it looks like it is working and doing what I am after (in the example above, the computation is stopped when i
becomes 3 and result
is set to Error "error"
), but I am not 100% sure if the implementation of the EitherBuilder
is fully correct. I would appreciate some feedback.
functional-programming f# monads
I want to implement an Either computation expression in F#. The computation executes a function inside a while
loop, and this function's return type is Result<int, string>
. The function might return an Error<string>
if some condition is met, and then the whole computation expression should be stopped.
This is the code I have so far to illustrate what I am trying to achieve:
type EitherBuilder () =
member this.Bind(x, f) =
match x with
| Ok s -> f s
| Error f -> Error f
member this.Return x = Ok(x)
member this.ReturnFrom x = x
member this.Zero() = Ok()
member this.Delay(f) = f
member this.Combine(x, f) = this.Bind(x, f)
member this.Run(f) = f()
member this.While(guard, body) =
if guard () then this.Bind(body (), fun _ -> this.While(guard, body))
else this.Zero()
let either = EitherBuilder ()
let mutable i = 0
let func i =
if i < 3 then Ok(i)
else Error("error")
let result = either
let mutable res = 0
while i < 10 do
let! output = func i
res <- output
i <- i + 1
return res
I have tested it and it looks like it is working and doing what I am after (in the example above, the computation is stopped when i
becomes 3 and result
is set to Error "error"
), but I am not 100% sure if the implementation of the EitherBuilder
is fully correct. I would appreciate some feedback.
functional-programming f# monads
edited Apr 28 at 13:18
Sam Onela
5,78461544
5,78461544
asked Apr 28 at 7:35
Carlos Rodriguez
1413
1413
add a comment |Â
add a comment |Â
1 Answer
1
active
oldest
votes
up vote
4
down vote
accepted
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: rn%O" typeof<'event>.Name ex
else failwithf "Unable To Construct a Failure of type %s from Unhandled Exception: rn%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)
add a comment |Â
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
4
down vote
accepted
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: rn%O" typeof<'event>.Name ex
else failwithf "Unable To Construct a Failure of type %s from Unhandled Exception: rn%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)
add a comment |Â
up vote
4
down vote
accepted
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: rn%O" typeof<'event>.Name ex
else failwithf "Unable To Construct a Failure of type %s from Unhandled Exception: rn%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)
add a comment |Â
up vote
4
down vote
accepted
up vote
4
down vote
accepted
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: rn%O" typeof<'event>.Name ex
else failwithf "Unable To Construct a Failure of type %s from Unhandled Exception: rn%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)
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: rn%O" typeof<'event>.Name ex
else failwithf "Unable To Construct a Failure of type %s from Unhandled Exception: rn%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)
edited May 3 at 13:14
answered May 3 at 12:40
Aaron M. Eshbach
3297
3297
add a comment |Â
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f193132%2ff-either-computation-expression-with-while-loop%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password