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?
RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using observables that makes it easier to compose asynchronous or callback-based code.
The Observable Class in RxJSRxJS implements observable as a class with a constructor, properties and methods. The most important methods in the observable class are subscribe and pipe : subscribe() lets us subscribe to an observable instance.
RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras (map, filter, reduce, every, etc) to allow handling asynchronous events as collections.
What is an Observable? An observable represents a stream, or source of data that can arrive over time. You can create an observable from nearly anything, but the most common use case in RxJS is from events. This can be anything from mouse moves, button clicks, input into a text field, or even route changes.
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 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 ;-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With