4

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.

Thomas
  • 10,933
  • 14
  • 65
  • 136
  • 2
    There is no way to combine existing expression builders, only write one from scratch. And while you could certainly do that, in practice it's almost never worth it. In this particular case your approach is the right one. – Fyodor Soikin Jun 12 '21 at 21:30
  • 2
    if you don't mind some advanced SRTP foo then there is a way to combine CEs - the FSharpPlus Project does define some *monad-transformers* - for exemple [`ResultT`](https://fsprojects.github.io/FSharpPlus/reference/fsharpplus-data-resultt-1.html) you can use to combine some CE with `Result` - if this overhead is worth for you I don't know of course – Random Dev Jun 13 '21 at 05:42
  • I agree, so far it's the only library that allows you to do that. I could help you if you post code that is self contained. Otherwise here's a [similar question](https://stackoverflow.com/questions/12376833/combine-f-async-and-maybe-computation-expression) – Gus Jun 13 '21 at 09:56
  • 1
    as I pointed in the question, it's a learning topic for me so the overhead / practicality is not relevant; I've started to look at the FSharpPlus lib and that looks very interesting. Monads / Applicatives are quite new to me and I would like to understand them in depth – Thomas Jun 13 '21 at 10:30
  • Precisely because it's a learning topic, maybe it's better to use a more agnostic sample code. That way you also increase the chances someone will grab your code and show you how to do it. – Gus Jun 13 '21 at 19:12
  • @gus, you made a good point, I did an edit with some simple pseudo functions. – Thomas Jun 13 '21 at 21:35
  • Now that I see your update, it looks like you're not trying to combine 2 CEs in the sense of composition, but rather to overload a bind operation of the CE in order to deal with 2 different types, that's something libs like FsToolkit does, but F#+ doesn't and since it doesn't follow any rule, other than convenience I doubt you can find a generic way to express it. – Gus Jun 14 '21 at 15:31
  • @gus yes, correct; I'm trying to find if there is a way to make a flexible CE by combining the bind operations of both. FsToolKit just seems to have specific code for the cases they support (and it very useful) – Thomas Jun 14 '21 at 15:57

1 Answers1

1

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
    }

JaggerJo
  • 734
  • 4
  • 15