How to implement custom Monad instance, especially for IObservable in FSharpPlus?

207 views Asked by At

FSharpPlus provided monad CE and several monad transformers, and I want to use ReaderT<'a, IObservable<'b>> with FSharpPlus's monad CE, which requires a definition of monad instance of IObservable.

An example of desired code is

let test (x: IObservable<int>) = 
    monad {
        let! a = x
        let! b = x
        return a + b
    }

Which is expected to translate to

let test (v: IObservable<int>) = 
    x.SelectMany(fun n -> Observable.Return(n + 1))

But the monad CE does not support IObservable out of box, and adding extension like following to the same module of test function above does not work.

type IObservable<'a> with
    static member Return (x: 'T) : IObservable<'T> =
        Observable.Return(x)

    static member (>>=) (x: IObservable<'T>, f: 'T->IObservable<'U>) : IObservable<'U> = 
        x.SelectMany(f)

How to define monad instance for type IObservable?


Update

update use case from

let test (x: IObservable<int>) = 
    monad {
        let! n = x
        return n + 1
    }

to current one to prevent unintentional relation to Functor.

Update

As @Gus mentioned in the answer, it may not easy to direct add monad instance for IObservable.

After some search, since Functor instance of IObservable works (support map and |>>), it seems free monad may be one solution.

The following code seems worked:

open System
open System.Reactive.Linq
open FSharpPlus
open FSharpPlus.Data

let rec interpret (p: Free<IObservable<'a>, 'a>) : IObservable<'a> =
    match Free.run p with
    | Choice1Of2 x -> Observable.Return(x)
    | Choice2Of2 p' -> p'.SelectMany(interpret)

let test =
    monad {
        let! a = Free.liftF (Observable.Range(0, 4).Select(fun n -> n * 10))
        let! b = Free.liftF (Observable.Range(0, 10))
        return a + b
    }
    |> interpret

As a result, what we need to do is add Free.liftF before all bind in monad CE for IObservable s, and add a interpret helper function to the end.

The interpret implementation may not be stacksafe (or maybe it is by IObservable.SelectMany implementation?).

Although this compiles and seems run correctly for simple cases, I'm wondering is this usage of free monad correct?

1

There are 1 answers

2
Gus On BEST ANSWER

At the time of writing this answer there is no way to have extension methods visible to trait constraints, although there is a longstanding RFC and PR in the F# compiler to implement it.

So, for the time being in order to add a type to an abstraction (like Monad) you need to either edit the type's source to add the required method or the abstraction source.

The former is basically impossible for IObservable I mean you can try submit them a PR, but the latter option is more feasible in the short term: submit a PR or at least open an issue in F#+ to add IObservable directly as Monad.