Thomas
Thomas

Reputation: 12107

how can I combine / compose computation expressions, in F#?

This is not for a practical need, but rather to try to learn something.

I am using FSToolKit's asyncResult expression which is very handy and I would like to know if there is a way to 'combine' expressions, such as async and result here, or does a custom expression have to be written?

Here is an example of my function to set the ip to a subdomain, with CloudFlare:

let setSubdomainToIpAsync zoneName url ip =

    let decodeResult (r: CloudFlareResult<'a>) =
        match r.Success with
        | true  -> Ok r.Result
        | false -> Error r.Errors.[0].Message

    let getZoneAsync (client: CloudFlareClient) =
        asyncResult {
            let! r = client.Zones.GetAsync()
            let! d = decodeResult r
            return!
                match d |> Seq.filter (fun x -> x.Name = zoneName) |> Seq.toList with
                | z::_ -> Ok z // take the first one
                | _    -> Error $"zone '{zoneName}' not found"
        }

    let getRecordsAsync (client: CloudFlareClient) zoneId  =
        asyncResult {
            let! r = client.Zones.DnsRecords.GetAsync(zoneId)
            return! decodeResult r
        }

    let updateRecordAsync (client: CloudFlareClient) zoneId (records: DnsRecord seq) =
        asyncResult {
            return!
                match records |> Seq.filter (fun x -> x.Name = url) |> Seq.toList with
                | r::_ -> client.Zones.DnsRecords.UpdateAsync(zoneId, r.Id, ModifiedDnsRecord(Name = url, Content = ip, Type = DnsRecordType.A, Proxied = true))
                | []   -> client.Zones.DnsRecords.AddAsync(zoneId, NewDnsRecord(Name = url, Content = ip, Proxied = true))
        }

    asyncResult {
        use client   = new CloudFlareClient(Credentials.CloudFlare.Email, Credentials.CloudFlare.Key)
        let! zone    = getZoneAsync client
        let! records = getRecordsAsync client zone.Id
        let! update  = updateRecordAsync client zone.Id records
        return! decodeResult update
    }

It is interfacing with a C# lib that handles all the calls to the CloudFlare API and returns a CloudFlareResult object which has a success flag, a result and an error.

I remapped that type to a Result<'a, string> type:

let decodeResult (r: CloudFlareResult<'a>) =
    match r.Success with
    | true  -> Ok r.Result
    | false -> Error r.Errors.[0].Message

And I could write an expression for it (hypothetically since I've been using them but haven't written my own yet), but then I would be happy to have an asyncCloudFlareResult expression, or even an asyncCloudFlareResultOrResult expression, if that makes sense.

I am wondering if there is a mechanism to combine expressions together, the same way FSToolKit does (although I suspect it's just custom code there).

Again, this is a question to learn something, not about the practicality since it would probably add more code than it's worth.


Following Gus' comment, I realized it would be good to illustrate the point with some simpler code:

function DoA : int -> Async<AWSCallResult<int, string>>
function DoB : int -> Async<Result<int, string>>

AWSCallResultAndResult {
    let! a = DoA 3
    let! b = DoB a
    return b
}

in this example I would end up with two types that can take an int and return an error string, but they are different. Both have their expressions so I can chain them as needed. And the original question is about how these can be combined together.

Upvotes: 4

Views: 414

Answers (1)

JaggerJo
JaggerJo

Reputation: 934

It's possible to extend CEs with overloads.

The example below makes it possible to use the CustomResult type with a usual result builder.


open FsToolkit.ErrorHandling

type CustomResult<'T, 'TError> =
    { IsError: bool
      Error: 'TError
      Value: 'T }

type ResultBuilder with

    member inline _.Source(result : CustomResult<'T, 'TError>) =
        if result.IsError then
            Error result.Error
        else
            Ok result.Value

let computeA () = Ok 42
let computeB () = Ok 23
let computeC () =
    { CustomResult.Error = "oops. This went wrong"
      CustomResult.IsError = true
      CustomResult.Value = 64 }

let computedResult =
    result {
        let! a = computeA ()
        let! b = computeB ()
        let! c = computeC ()

        return a + b + c
    }

Upvotes: 1

Related Questions