【问题标题】:Is RxJS.Observable a monad?RxJS.Observable 是一个单子吗?
【发布时间】:2018-07-26 15:46:53
【问题描述】:

Observable 真的是一个单子吗?它是否遵守 Monad 法则 (https://wiki.haskell.org/Monad_laws)?在我看来不像这样。但也许我的理解是错误的,有人可以对这个问题有所了解。我目前的推理是(我使用 :: 来表示“是善良的”):

1) 左身份: return a >>= f ≡ f a

var func = x => Rx.Observable.of(10)

var a = Rx.Observable.of(1).flatMap(func) :: Observable
var b = func(1)                           :: ScalarObservable

哈斯克尔:

func = (\_ -> putStrLn "B")

do { putStrLn "hello"; return "A" } >>= func   :: IO ()

func "A"                                       :: IO ()

所以左身份不适用于 Observable。 Observable 显然不是 ScalarObservable。在 Haskell 中,类型是相同的 - IO ()

2) 正确的身份: m >>= return ≡ m

var x = Rx.Observable.of(1);

x.flatMap(x => Observable.of(x)) :: Observable
x                                :: ScalarObservable

哈斯克尔:

Just 2 >>= return  :: Num b => Maybe b
Just 2             :: Num a => Maybe a

与左身份相同的情况。可观察的!== ScalarObservable。而在 Haskell 中,类型保持不变,它是一个 Maybe,里面有一个 Num。

3) 关联性

(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)

var x = Rx.Observable.of(10)

var func1 = (x) => Rx.Observable.of(x + 1)
var func2 = (x) => Rx.Observable.of(x + 2)


x.flatMap(func1).flatMap(func2)         :: Observable
x.flatMap(e => func1(e).flatMap(func2)) :: Observable

哈斯克尔:

add2 x = Just(x + 2)
add1 x = Just(x + 1)

Just 2 >>= add1 >>= add2             :: Num b => Maybe b
Just 2 >>= (\x -> add1(x) >>= add2)  :: Num b => Maybe b

这似乎是唯一适用于 Observable 的定律。 但我不知道,也许这不应该像我那样推理。你怎么看?

【问题讨论】:

  • 我很确定 ScalarObservable 只是 Observable<Number> 的优化专业化。请注意,法律并没有规定双方需要返回完全相同的值,而只是规定他们需要表现相同。
  • 谢谢@Bergi,我希望得到这样的答案。最后,在wiki.haskell.org/Monad_laws 中,我们可以读到 “这里,p ≡ q 只是意味着您可以将 p 替换为 q,反之亦然,并且您的程序的行为不会改变:p 和 q 是等价物。”.这是否意味着如果我们执行x.flatMap(x => Observable.of(x)).subscribe(a => console.log(a))x.subscribe(a => console.log(a)) 并最终得到相同的副作用,那么这足以证明左身份 可以支持Observable?
  • 这是正确的身份,但总的来说是的

标签: javascript haskell rxjs observable


【解决方案1】:

tldr; 是的。


JavaScript 是一种带有鸭子类型的动态语言,因此在运行时,Observable 类的实例等效ScalarObservable 的实例。 RxJS 本身是用 TypeScript 编写的,这些不规则性不会出现在类型中,它们是 - 正如@Bergi 在评论中所写的那样 - 一种优化。另一方面,您是完全正确的:在名义类型系统中,类型不匹配可能是一个真正的问题,甚至是编译时错误。


现在,回答这个问题本身 - 请查看带有 RxJS 绑定的Purescript library

foreign import data Observable :: Type -> Type

instance monoidObservable :: Monoid (Observable a) where
  mempty = _empty

instance functorObservable :: Functor Observable where
  map = _map

instance applyObservable :: Apply Observable where
  apply = combineLatest id

instance applicativeObservable :: Applicative Observable where
  pure = just

instance bindObservable :: Bind Observable where
  bind = mergeMap

instance monadObservable :: Monad Observable

-- | NOTE: The semigroup instance uses `merge` NOT `concat`.
instance semigroupObservable :: Semigroup (Observable a) where
  append = merge

instance altObservable :: Alt Observable where
  alt = merge

instance plusObservable :: Plus Observable where
  empty = _empty

instance alternativeObservable :: Alternative Observable

instance monadZeroObservable :: MonadZero Observable

instance monadPlusObservable :: MonadPlus Observable

instance monadErrorObservable :: MonadError Error Observable where
  catchError = catch

instance monadThrowObservable :: MonadThrow Error Observable where
  throwError = throw

假设 Purescript 类型是正确的:除了是常规的 Monad 之外,Observable 还符合 MonadPlusMonadError 类。 MonadPlus 允许合并计算,而MonadError 允许中断或跳过部分计算(在Observable 的情况下,我们也可以轻松地重试计算)。 Observable 不仅是一个单子,而且是一个非常强大的单子——也许甚至是主stream$中使用的最强大的单子。

我没有任何正式的证明,但可以简要描述如何使用 Observable 来建模或替换 https://wiki.haskell.org/All_About_Monads 中描述的 monad。

可能 可能不会返回结果的计算

非结果可以表示为常规 JS undefinedEMPTY 流。

错误 可能失败或抛出异常的计算

您可以抛出常规的 JS 错误或从 monadic 绑定返回更多惯用的 throwError。错误可以被捕获,然后处理或用于重试计算。抛出错误会立即停止正在进行的计算。

列表 可以返回多个可能结果的非确定性计算

List 是 Observable 的小弟,完全没有时间维度。任何可以通过列表上的操作表达的东西都可以精确地映射到可观察对象上的操作。您可以通过Observable.from 轻松提升列表并通过.toList() 降级为可观察列表。作为原生,列表性能将比可观察的要好得多。但请记住,list 是 Eager 和 observable 惰性的,因此在某些情况下 observable 可能会优于 list。

IO 执行 I/O 的计算

任何 IO 操作(网络、磁盘等)都可以轻松包装/提升到可观察世界。

状态 维持状态的计算

行为主题

Reader 从共享环境中读取的计算

从消费者的角度来看,Observable 的实例来自哪里并不重要。例如:如果您将配置声明为可观察的,您可以轻松更改提供值的确切环境。

Writer 除了计算值之外还写入数据的计算

最简单的选择是返回两个流,一个带有值,另一个带有日志/辅助数据。

继续 可以中断和重新启动的计算

要中断计算,您可以抛出错误,请使用运算符,例如.switchMap.takeUntil、明确取消订阅或.mergeMapEMPTY。访问某种形式的缓存以从任意步骤重新启动 确定性 计算非常简单:只需将计算拆分为更小的可观察对象,并在计算后缓存它们的结果;重新启动时,仅当缓存为空时才运行计算 - 否则使用缓存值。


如果您决定使用 observables 来表示您的计算结构 - 您不仅可以建模/替换实践中使用的最常见的 monad,而且您的计算在风格上会自动响应。此外,如果您只坚持 observable,您的计算将是同质的,这意味着很少或不需要 monad 转换器以及它们引入的意外复杂性。 我的工作假设是,可观察类型为表达异步计算的结构提供了一些局部(甚至全局)最大值。例如:Observable 提供的不是一个,不是两个,而是 三个! monadic 以不同的语义绑定:mergeMapswitchMapexhaustMap(如果您想知道:concatMap 实际上是一个特殊的mergeMap 的情况)。这个事实本身就表明 observable 是一个非常有趣的数学结构。


奖金

Observable 被称为流,流(通常)是 [commonads]​​ (https://bartoszmilewski.com/2017/01/02/comonads/)。这是否意味着 observable 不仅是一个单子,也是一个共单子?

Erik Meijer twit's:

@rix0rrr 有一段时间,Rx 有一个 ManySelect 运算符。 Rx 既是单子又是共单子。 144 个字符太短,无法解释。对不起;-)

【讨论】:

猜你喜欢
  • 2012-05-16
  • 1970-01-01
  • 2021-12-23
  • 2015-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多