admin管理员组文章数量:1202803
Is Observable really a monad? Does it abide by Monad laws ()? Doesn't seem to me like it does. But maybe my understanding is wrong and somebody can shed some light on the issue. My current reasoning is (I'm using :: to denote "is of kind"):
1) Left identity: 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
HASKELL:
func = (\_ -> putStrLn "B")
do { putStrLn "hello"; return "A" } >>= func :: IO ()
func "A" :: IO ()
So left identity doesn't hold for Observable. Observable clearly isn't ScalarObservable. In Haskell, the types are the same - IO ().
2) Right identity: m >>= return ≡ m
var x = Rx.Observable.of(1);
x.flatMap(x => Observable.of(x)) :: Observable
x :: ScalarObservable
HASKELL:
Just 2 >>= return :: Num b => Maybe b
Just 2 :: Num a => Maybe a
The same situation as with the left identity. Observable !== ScalarObservable. Whereas in Haskell, the type stays the same, it's a Maybe with a Num inside it.
3) Associativity
(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
HASKELL:
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
This is the only law that seems to hold for Observable. But I don't know, maybe this should not be reasoned in the way I did. What do you think?
Is Observable really a monad? Does it abide by Monad laws (https://wiki.haskell.org/Monad_laws)? Doesn't seem to me like it does. But maybe my understanding is wrong and somebody can shed some light on the issue. My current reasoning is (I'm using :: to denote "is of kind"):
1) Left identity: 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
HASKELL:
func = (\_ -> putStrLn "B")
do { putStrLn "hello"; return "A" } >>= func :: IO ()
func "A" :: IO ()
So left identity doesn't hold for Observable. Observable clearly isn't ScalarObservable. In Haskell, the types are the same - IO ().
2) Right identity: m >>= return ≡ m
var x = Rx.Observable.of(1);
x.flatMap(x => Observable.of(x)) :: Observable
x :: ScalarObservable
HASKELL:
Just 2 >>= return :: Num b => Maybe b
Just 2 :: Num a => Maybe a
The same situation as with the left identity. Observable !== ScalarObservable. Whereas in Haskell, the type stays the same, it's a Maybe with a Num inside it.
3) Associativity
(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
HASKELL:
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
This is the only law that seems to hold for Observable. But I don't know, maybe this should not be reasoned in the way I did. What do you think?
Share Improve this question asked Jul 26, 2018 at 15:46 Mateusz WitMateusz Wit 4855 silver badges10 bronze badges 3 |1 Answer
Reset to default 22tldr; Yes.
JavaScript is a dynamic language with duck typing so in runtime, instance of an Observable
class is equivalent to an instance of ScalarObservable
. RxJS itself is written in TypeScript and these irregularities do not surface up in types and they are - exactly as @Bergi wrote in a comment - an optimisation. On the other hand, you are completely right: in a nominal type system type mismatch could be a real problem and even a compile time error.
Now, answering the question itself - please take a look at a Purescript library with bindings to RxJS:
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
Assuming Purescript types are correct: apart from being a regular Monad
, Observable
conforms to MonadPlus
and MonadError
classes. MonadPlus
allows to combine computations, while MonadError
allows to interrupt or skip some part of computations (in case of Observable
we can easily retry computations as well).
Observable
is not only a monad, but a very powerful one - maybe even the most powerful monad used in the mainstream$.
I do not have any formal proofs, but can shortly describe how to use Observable to model or replace monads describe in https://wiki.haskell.org/All_About_Monads.
Maybe Computations which may not return a result
Non-result can be represented as regular JS undefined
or an EMPTY
stream.
Error Computations which can fail or throw exceptions
You can throw regular JS errors or return more idiomatic throwError
from monadic bind. An error can be catch'ed and then handled or use to retry computations. Throwing an error immediately stops ongoing computations.
List Non-deterministic computations which can return multiple possible results
List is kind of a younger brother of Observable, lacking entirely the time dimension. Anything that can be expressed via operations on a list can be exactly mapped to operations on an observable. You can easily lift a list via Observable.from
and downgrade to observable with .toList()
. Being native, list performance is going to be much better than observable's. But remember that list is eager and observable lazy, so in some cases observable may outperform list.
IO Computations which perform I/O
Any IO operations (network, disk etc) can be easily wrapped / lifted to the observable world.
State Computations which maintain state
BehaviorSubject
Reader Computations which read from a shared environment
From a consumer perspective it does not matter at all where an instance of Observable comes from. For example: if you declared your config as an observable you can easily change the exact environment from where the value(s) are provided.
Writer Computations which write data in addition to computing values
The simplest option is to return two streams one with values and the other with logs / auxiliary data.
Cont Computations which can be interrupted and restarted
To interrupt computations you can throw an error, use an operator e.g. .switchMap
, .takeUntil
, explicitly unsubscribe or .mergeMap
to EMPTY
. Having access to some form of a cache restarting deterministic computations from arbitrary step is pretty trivial: just split your computations to smaller observables and cache their results once computed; when restarted run computations only if cache empty - otherwise use cached value.
If you decide to use observables to represent structure of your computations - you not only can model / replace the most common monads used in practice, but your computations are automatically reactive in flavour. Moreover if you stick to only observable your computations are going to be homogenous, which means there is very little or no need for monad transformers and accidental complexity introduced by them.
My working hypothesis is that observable type offers some local (or even global) maximum for expressing structure of asynchronous computations. For example: Observable offers not one, not two, but three! monadic binds with different semantic: mergeMap
, switchMap
, exhaustMap
(if you wonder: concatMap
is actually a special case of mergeMap
). This very fact on its own is kind of indication that observable is a very interesting mathematical structure.
A bonus
Observable is said to be a stream and streams (in general) are [commonads] (https://bartoszmilewski.com/2017/01/02/comonads/). Does it mean that observable is not only a monad but a comonad as well?
Erik Meijer twit's:
@rix0rrr For a while Rx had a ManySelect operator. Rx is both a monad and a comonad. 144 characters is too short to explain that. Sorry ;-)
本文标签: javascriptIs RxJSObservable a monadStack Overflow
版权声明:本文标题:javascript - Is RxJS.Observable a monad? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1738658007a2105252.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
ScalarObservable
is just an optimisation specialisation forObservable<Number>
. Notice that the law doesn't state the two sides need to return the exact same value, but just that they need to behave equally. – Bergi Commented Jul 26, 2018 at 15:53x.flatMap(x => Observable.of(x)).subscribe(a => console.log(a))
andx.subscribe(a => console.log(a))
and we end up with the same side effect, then it's enough proof for the Left identity to hold for Observable? – Mateusz Wit Commented Jul 26, 2018 at 16:01