简体   繁体   中英

Is RxJS.Observable a monad?

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 ≡ fa

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 -> fx >>= 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?

tldr; 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 main stream$ .

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 eg .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 ;-)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM