Search code examples
.netasynchronousfunctional-programmingf#mailboxprocessor

F# Async let! & return! computation expression


I have started to read stuff about computation expressions and so far as I understand - it has some hidden implementations that are default and custom.

I will provide things that I understand and please correct me.

For example, in this case we define a custom implementation to use let!. So that every expression bound to the let! inside the logger block will be logged to the console.

type LoggingBuilder() =
    let log p = printfn "expression is %A" p

    member this.Bind(x, f) =
        log x
        f x

    member this.Return(x) = x

let logger = new LoggingBuilder()

let loggedWorkflow =
    logger {
        let! x = 42
        let! y = 43
        let! z = x + y
        return z
    }


I cant remember precise but I have read that if we don't provide an implementation to it - it has some default built in. For example some workflow that when it has received None, it will stop the whole workflow and will return just none, if it will return Some - the code will continue -> is this default or not?

Since keywords that are followed by the exclamation mark have some extra functionality behind the scenes, what is it inside the async {} block?

Take this example.

let printerAgent =
    MailboxProcessor.Start
        (fun inbox ->

            // the message processing function
            let rec messageLoop () =
                async {

                    // read a message
                    let! msg = inbox.Receive()

                    // process a message
                    printfn "message is: %s" msg

                    // loop to top
                    return! messageLoop ()
                }

            // start the loop
            messageLoop ())

I presume that the let! msg = inbox.Receive() will stop the workflow if it receives a None. About return! I really have no idea.


Solution

  • No, there are no default implementations for computation expression methods. If you want want special behavior for Async<'T option>, you can add an extension method to AsyncBuilder. It looks like you want to short-circuit an Async<unit>, so you would want something like this:

    type FSharp.Control.AsyncBuilder with
    
        member async.Bind(x: Async<'T option>, f: 'T -> Async<unit>) =
            async.Bind(x, function
                | Some x -> f x
                | None -> async.Return())
    

    The computation expression can resolve the overload between several Bind implementations, although you need to be careful: if the types are ambiguous, F# will choose a method implemented on the type itself (in this case, the built-in Bind) over an extension method.

    // Here `x` is used as an `int`, so F# knows that it needs to use
    // my Bind implementation.
    async {
        let! x = myAsyncIntOption
        let y = x + 1
        return ()
    }
    
    // Here the type of `x` is unspecified, so F# chooses to use
    // the built-in Bind implementation and `x` has type `int option`.
    async {
        let! x = myAsyncIntOption
        return ()
    }
    
    

    Now, I've said what can be done, but I wouldn't recommend actually doing this. Instead I would do something more explicit:

    let printerAgent =
        MailboxProcessor.Start
            (fun inbox ->
    
                // the message processing function
                let rec messageLoop () =
                    async {
    
                        // read a message
                        match! inbox.Receive() with
                        | None -> return ()
                        | Some msg ->
    
                            // process a message
                            printfn "message is: %s" msg
    
                            // loop to top
                            return! messageLoop ()
                    }
    
                // start the loop
                messageLoop ())