如何实现自定义 Monad 实例,尤其是 FSharpPlus 中的 IObservable?

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

FSharpPlus provided monad CE 和几个 monad 转换器,我想将 ReaderT<'a, IObservable<'b>> 与 FSharpPlus 的 monad CE 一起使用,这需要定义 IObservable 的 monad 实例。

所需代码的示例是

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

预计将翻译成

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

但是 monad CE 不支持开箱即用的 IObservable,并且在上面的 test 功能的同一模块中添加如下扩展不起作用。

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)

如何为 IObservable 类型定义 monad 实例?


更新

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

到当前一个以防止与 Functor 的无意关系。

更新

正如@Gus 在回答中提到的,直接为 IObservable 添加 monad 实例可能并不容易。

经过一些搜索,由于 IObservable 的 Functor 实例有效(支持 map|>>),似乎免费的 monad 可能是一种解决方案。

以下代码似乎有效:

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

因此,我们需要做的是在monad CE for IObservable 中的所有bind 之前添加Free.liftF,并在最后添加一个interpret 辅助函数。

interpret 实现可能不是堆栈安全的(或者可能是 IObservable.SelectMany 实现?)。

尽管对于简单的情况,这编译并且看起来 运行 正确,但我想知道这种免费 monad 的用法是否正确?

在撰写此答案时,无法使扩展方法对特征约束可见,尽管 F# 编译器中有 longstanding RFC 和 PR 来实现它。

因此,目前为了将类型添加到抽象(如 Monad),您需要编辑类型的源以添加所需的方法或抽象源。

前者基本不可能 IObservable 我的意思是你可以尝试向他们提交 PR,但后一种选择在短期内更可行:提交 PR 或至少在 F# 中打开一个问题+ 将 IObservable 直接添加为 Monad。