I’ve been wanting to write code like this in F#, and know that any exceptions within a bound expression in an audit { } block will not only get caught, but that an external auditing service will get notified that the operation has failed.
let agent = | |
AutoCancelAgent.Start(fun inbox -> async { | |
while true do | |
audit { | |
use! client = audit { return getClient () } | |
Log "Checking email..." | |
do! audit { return CollectMessages client } | |
expunge client | |
} |> doAuditedProcess | |
do! Async.Sleep(match pollInterval with Interval s -> s) | |
}) |
Unfortunately, it turns out my code in my post on error handling ( https//blog.mavnn.co.uk/playing-with-error-handling-strategies ) was flawed in its ability to handle errors. The irony has not escaped me.
The issue is with the eager evaluation of arguments to the TryFinally method of the builder. If it takes you a while to work out what that means, don’t worry: it took me about 2 days to wrap my head round it and work out how to correct the code to make it behave as I would have expected.
To make things work correctly, the type returned by the computational expression pretty much has to be a deferred function of some kind.
So, the Interface, now renamed IAuditBuilder, gains a couple of helper functions and becomes:
open System | |
type AuditedProcess<'T> = unit -> Option<'T> | |
let runAuditedProcess (auditedProcess : AuditedProcess<_>) = | |
auditedProcess () | |
let doAuditedProcess (auditedProcess : AuditedProcess<_>) = | |
runAuditedProcess auditedProcess |> ignore | |
type IAuditBuilder = | |
abstract Bind : AuditedProcess<'T> * ('T -> AuditedProcess<'U>) -> AuditedProcess<'U> | |
abstract Delay : (unit -> AuditedProcess<'T>) -> AuditedProcess<'T> | |
abstract Return : 'T -> AuditedProcess<'T> | |
abstract ReturnFrom : AuditedProcess<'T> -> AuditedProcess<'T> | |
abstract TryFinally : AuditedProcess<'T> * (unit -> unit) -> AuditedProcess<'T> | |
abstract Using : 'T * ('T -> AuditedProcess<'U>) -> AuditedProcess<'U> when 'T :> IDisposable | |
abstract Zero : unit -> AuditedProcess<'T> |
The implementation of the TestAuditBuilder (only logs to console on error) becomes:
type TestErrorBuilder () = | |
member this.Bind (expr, func) = | |
try | |
match expr () with | |
| None -> fun () -> None | |
| Some r -> | |
func r | |
with | |
| _ as e -> | |
printfn "%A" e | |
fun () -> None | |
member this.Delay (f: unit -> AuditedProcess<'U>) : AuditedProcess<'U> = | |
this.Bind (this.Return (), f) | |
member this.Return<'T> (value : 'T) : AuditedProcess<'T> = | |
fun () -> Some value | |
member this.ReturnFrom (value) = | |
value | |
member this.TryFinally (expr, comp) = | |
try expr | |
finally comp () | |
member this.Using (res : #System.IDisposable, expr) = | |
this.TryFinally(expr res, fun () -> res.Dispose()) | |
member this.Zero () = | |
fun () -> None | |
interface IAuditBuilder with | |
member this.Bind (expr, func) = this.Bind (expr, func) | |
member this.Delay (f) = this.Delay (f) | |
member this.Return (value) = this.Return (value) | |
member this.ReturnFrom (value) = this.ReturnFrom(value) | |
member this.TryFinally (expr, comp) = this.TryFinally (expr, comp) | |
member this.Using (res : #System.IDisposable, expr) = this.Using (res, expr) | |
member this.Zero () = this.Zero () |
So: many thanks to Johann Deneux for patiently pointing out to me what the flaw in the original code was. I hope this example of a lazy computational expression is useful to other starting out down this rabbit hole of monadic weirdness. At least the resulting code looks pretty nice and readable now that the builder is fixed.