2
votes

I'm trying to understand the reader monad transformer. I'm using FSharpPlus and try to compile the following sample which first reads something from the reader environment, then performs some async computation and finally combines both results:

open FSharpPlus
open FSharpPlus.Data

let sampleReader = monad {
    let! value = ask
    return value * 2
}

let sampleWorkflow = monad {
    do! Async.Sleep 5000
    return 4
}

let doWork = monad {
    let! envValue = sampleReader
    let! workValue = liftAsync sampleWorkflow
    return envValue + workValue
}

ReaderT.run doWork 3 |> Async.RunSynchronously |> printfn "Result: %d"

With this I get a compilation error at the line where it says let! value = ask with the following totally unhelpful (at least for me) error message:

Type constraint mismatch when applying the default type 'obj' for a type inference variable. No overloads match for method 'op_GreaterGreaterEquals'.

Known return type: Async

Known type parameters: < obj , (int -> Async) >

It feels like I'm just missing some operator somewhere, but I can't figure it out.

1

1 Answers

3
votes

Your code is correct, but F# type inference is not that smart in cases like this.

If you add a type annotation to sampleReader it will compile fine:

let sampleReader : ReaderT<int,Async<_>> = monad {
    let! value = ask
    return value * 2
}

// val sampleReader : FSharpPlus.Data.ReaderT<int,Async<int>> =
//  ReaderT <fun:sampleReader@7>

Update:

After reading your comments. If what you want is to make it generic, first of all your function has to be declared inline otherwise type constraints can't be applied:

let inline sampleReader = monad ...

But that takes you to the second problem: a constant can't be declared inline (actually there is a way but it's too complicated) only functions can.

So the easiest is to make it a function:

let inline sampleReader () = monad ...

And now the third problem the code doesn't compile :)

Here again, you can give type inference a minimal hint, just to say at the call site that you expect a ReaderT<_,_> will be enough:

let inline sampleReader () = monad {
    let! value = ask
    return value * 2
}

let sampleWorkflow = monad {
    do! Async.Sleep 5000
    return 4
}

let doWork = monad {
    let! envValue = sampleReader () : ReaderT<_,_>
    let! workValue = liftAsync sampleWorkflow
    return envValue + workValue
}

ReaderT.run doWork 3 |> Async.RunSynchronously |> printfn "Result: %d"

Conclusion:

Defining a generic function is not that trivial task in F#. If you look into the source of F#+ you'll see what I mean.

After running your example you'll see all the constraints being generated and you'll probably noted how the compile-time increased by making your function inline and generic.

These are all indications that we're pushing F# type system to the limits.

Although F#+ defines some ready-to-use generic functions, and these functions can sometimes be combined in such a way that you create your own generic functions, that's not the goal of the library, I mean you can but then you're on your own, in some scenarios like exploratory development it might make sense.