Comments (23)
I think Fantasy Land has a communication problem. Functional programmers really care about precise terminology, and there is good reasons for that. But it doesn't help when people want to understand quickly what the value proposition is. It can quickly seem like ivory tower heady nonsense.
I've slowly but surely worked my way through the terminology, embraced the concepts and now I am happily using Fantasy Land and Ramda everywhere (and have been for years now).
So I hope I can save everyone a lot of time by communicating what Fantasy Land is, why its hugely important to the ecosystem, why it is exciting and important that mainstream libraries adopt it.
So here's my pitch:
Fantasy land is a specification for interoperability with an infinitude of types. It lets you write code once, that works again and again in different contexts. It will work against arrays, streams, futures, tasks, maybes, eithers, trees, infinite structures - you name it!
- Fantasy Land support is the 2nd most popular feature request for Lodash
- Fantasy Land doesn't require changing behaviour of your existing methods, the spec prefixes functions so as long as
stream['fantasy-land/map']
implements the spec, its okay forstream.map
to deviate. - Other main_stream_ libraries implement fantasy land including most and flyd
- Many non stream libraries implement fantasy land
- Ramda (and hopefully one day lodash) automatically dispatches to the underlying types method, so
R.map( x=> x* 2)
will run against most streams, folktale futures, sanctuary maybe's, plain old arrays etc
that's not really compelling per se if Observable is indeed a standard. We'll support in flatMap with Observables, Promises, Iterators (Arrays, Maps, Sets, Generators), and AsyncIterators so that gives us plenty of choices. Fantasyland doesn't buy us anything here
This is the thing though, are we going to write custom code for every data type with every new release to the language? Or should we all embrace a logical, lawful, proven spec that popular libraries already implement, so that the next language feature that is eventually added, doesn't require new code.
Maybe in 2018 we get channels, or goroutines or optionals. Do we want to keep writing new code for the same situations for every data structure?
It also shouldn't be Rx (or any other libraries) responsibility to implement support for every new data structure the language introduces. That is the purview of a language. You should get that for free. When ECMAScript introduces a new data structure, your users should immediately be able to use it with Rx without a point release.
I've written more about the value Fantasy Land brings here: https://james-forbes.com/?/posts/the-perfect-api
The article completely avoids words like Monad or Applicative because you don't need to understand these concepts to get value from the spec. The spec provides value to library consumers, but only library authors need to understand the low level details.
Rx has a huge API surface. And that hurts adoption. Wouldn't it be nice if Rx could just piggy back on existing methods in utility libraries like lodash or ramda? That is the JS world we could be living in. Jumping from most to Rx without requiring changes to any code other than imports? That is the JS world we could be living in. Switch libraries without needing to check documentation - that is the JS world we could be living in.
Realistically the language is influenced by libraries like Rx and Lodash. By implementing the spec you could influence the language to implement it. And in turn that would be saving countless dev hours reading documentation and articles on each and every data structure and library release for behaviour that should be standardized generically and learnt once.
If anyone has any questions or doubts about this, I would really like to help. I am not affiliated with them, I'm not a contributor, but as a user, I really see the value and I want JS to be better. I think a shared, lawful API across all data structures in the language and the community will make for better code, better products and happier devs.
I know how hard it can be to grok the Fantasy Land spec. But the community around the spec is also very helpful, you can ask the community questions here:
https://gitter.im/fantasyland/fantasy-land
from rxjs.
@Blesh I completely understand your point of view. But I want to underscore, the naming within the Fantasy Land spec isn't imporant - Rx users don't need to know the terms, but they would benefit from a consistent API across libraries
I personally think the terminology starts to be come helpful once you go further down that path, but an Rx user can be blissfully ignorant of the Monadic guarantees that lie within the code base. Think of it like fluoride in the water. We all get less decays, but we don't need to think about it while we drink water.
For anyone else reading this, here is a simple/rough definition of a Monad, to prove they aren't scary.
A Monad is a type that holds a value within it. Like an array, or a stream or a promise. The value is wrapped in a container, and that containment gives you guarantees like null safety, or avoiding race conditions. The applications are endless.
Monad's obey some laws. We call them laws because they come from maths, but its just an interface that provides many guarantees.
Rx observables almost follow Monadic laws. But in order to have a consistent api across libraries and data structures we need to remove some functionality. One example would be the map
method shouldn't automatically swallow a promise.
Of course, you wouldn't need to change the actual map
method in Rx, just a hidden method that is namespaced fantasy-land/map
. Other libraries and types can call this hidden method. And this allows you to compose and combine different types in powerful ways. And I'd be more than happy to demonstrate a number of ways if anyone is curious.
Here is a quick one:
You might be familiar with Promise.all
, given an array of promises, we get a Promise of an array of resolved values.
Well if A+ Promises were monadic, and RxJS was monadic, we could use exactly the same function to perform that conversion. Given an array of streams, we can get a stream of an array of values.
This already works with libraries like most.js and promise alternatives like Fluture
Here is the same code using Ramda for both types.
R.sequence(Stream.of, [streamA, streamB, streamC]).map(
([a,b,c]) => a + b + c
)
And here is the exact same code for Futures
R.sequence(Future.of, [futureA, futureB, futureC]).map(
([a,b,c]) => a + b + c
)
We can create a special Stream.all
method by just supplying the stream constructor
Stream.all = R.sequence(Stream.of)
Stream.all([streamA, streamB, streamC])
Without getting too carried away; we don't need to use an array, it could be any container. For example a stream of streams that resolves to a stream of values.
I could pick up any FantasyLand library and run sequence
against it and know exactly how it works, what the inputs are, what the outputs are for free. I don't need to study the docs, or watch a webinar. I can get immediate value. That is powerful. That is the value propositition.
sequence
is derived from a more general function traverse
which is derived from an even more generic method ap
. All ap
does is teach a function how to interact with your data type. From there we can do some amazing things in a general way.
You can benefit from this stuff without needing to know what a Monad is.
Not implementing a specification because of naming would be a shame. I don't come from a functional background, so I struggle with the terminology sometimes - but that is all it is, terminology.
Functional programming is actually very simple once you get past the terminology (and occasionally syntax) used. You learn a few rules that will come up over and over and continue to pay dividends.
One of my goals is to bridge that cultural divide, because I understand how wide it is. But I also know how well suited JS is at functional programming. It works really well, and I don't want this knowledge and these applications to be a niche thing when the language does it so well.
from rxjs.
I think Fantasy Land has a communication problem. Functional programmers really care about precise terminology, and there is good reasons for that. But it doesn't help when people want to understand quickly what the value proposition is. It can quickly seem like ivory tower heady nonsense.
It's more than a communication problem. The precise terminology frankly sucks. Just as one shouldn't let just any engineer design a site, people shouldn't let mathematicians name things. Ever.
If we wanted to kill adoption of Rx, pushing out types like "Monad", "Monoid" and "Setoid" would do it.
from rxjs.
Just use most.js
from rxjs.
@trxcllnt What have you been smoking?
from rxjs.
Fantasy Land is not about pureness of FP. It is about composability. Considering the fact, that RxJS is a library designed for integration into larger systems, it should probably be a priority.
There is a great talk by Brian Lonsdorf about the issue of composability: "Hey Underscore, You're Doing It Wrong!". RxJS reminds me of underscore in 2013.
I haven't looked into RxJS for a long time, so please correct me if some things changed lately. Also I don't think that converting Observable to a Monad will be as simple as aliasing flatMap, but I might be wrong on this one.
Sidenote: there is a monadic stream implementation called most.
from rxjs.
To be continue with what @JAForbes is saying, conforming to the FL spec doesn't mean you need to change any of your current implementation, nor does it mean you have to restrict yourself to just what FL has. Those two ideas contradict the spirit of the spec.
If you happen to have a method foo
where obs.foo(x => x)
is "equivalent" to obs
and for any f
and g
, obs.foo(x => f(g(x)))
is "equivalent" to obs.foo(g).foo(f)
βfor whatever your definition of "equivalent" isβthen you can conform to the Functor
algebra and shove the appropriately namespaced function onto your prototype:
const fl = require('fantasy-land');
Observable.prototype[fl.map] = Observable.prototype.foo
Notice that the "fantasy land map" doesn't do anything different from what your original foo
did. It's just a namespaced alias for the method you and your current users know and love. And, if you don't want the dependency:
Observable.prototype['fantasy-land/map'] = Observable.prototype.foo
Your current API doesn't need to change at all. And if you don't have a foo
like that, then you don't conform to Functor
, not a big deal. If you decide at a later date that you want your foo
to behave differently, then you stop conforming to Functor
. No problem there either. If you decide you want to conform to different algebras at a later date, then do that.
You aren't stuck with anything, you don't have to change anything, you don't have to implement everything in FL, and you definitely don't have to restrict yourself to just what FL has. Importantly, if you know that you have a method that behaves like traverse
or something, but you don't want to conform to Traversable
, then simply don't conform to it.
from rxjs.
I think there might be some confusion around the kind of interoperability the fantasy-land spec facilitates.
While the Promises A+ spec facilitates this kind of interoperability:
X.foo().then(Y.bar).then(anotherStep)
(where X.foo
and Y.bar
are functions from different libraries that return different implementations of Promises)
Fantasy Land is instead concerned with this kind of interoperability:
R.lift((a, b)=> a + b)(S.Just(1), S.Just(2))
Where S
is a library that produces objects that implement the fantasy-land Applicative interface, and R
is a library that can work with those objects at the level of the Applicative interface.
While Observables Specification buys you interoperability with other Streams libraries, Fantasy Land doesn't buy you this kind of interoperability, because it doesn't define Stream-specific interfaces (eg: subscribe
).
What Fantasy Land does buy you is interoperability at the level of "Containers" of values (Functors, Monads, Applicatives, etc), for those operations where it is not necessary to know the semantics of the Container. eg transforming X(Y(a))
to Y(X(a))
, or turning f
, X(a)
and X(b)
into X(f(a,b))
.
(This lets people write code "once and for all" that will work with any "Containers" regardless of whether those containers represent streaming values, Future values, Maybe values, Optional values, etc)
from rxjs.
Any progress on this? I agree that FL compliance is extremely important, both to RxJS and to users of JavaScript in general. Can't stress this highly enough.
As for AndrΓ©'s comment, I love cycle.js and most.js, but I work for a large bank and they are extremely risk-averse. Unless a library/framework has sufficient momentum and support, they simply won't use it. We're currently using redux-observable, which uses RxJS under the covers. I wanted to use redux-most, but it was just too risky. But it is sad that we have to choose between low-risk and future-proofing.
I also find it sad how many devs on both sides of the FP/OOP line resort to snide comments instead of intelligent and open-minded discussion. These days what you say online lives forever. Speaking from experience, it might be a good idea to consider how embarrassing a post will look two or five or ten years from now before submitting it.
from rxjs.
I should clarify: I vote yes assuming it's not much more work than aliasing flatMap
to chain
.
@mattpodwysocki interoperability with other fantasy-land conforming libs without special-casing (e.g. Promises).
from rxjs.
What does conforming to the specification buy us exactly? And is it worth the effort other than the pureness of FP being applied here?
from rxjs.
@trxcllnt that's not really compelling per se if Observable is indeed a standard. We'll support in flatMap
with Observables, Promises, Iterators (Arrays, Maps, Sets, Generators), and AsyncIterators so that gives us plenty of choices. Fantasyland doesn't buy us anything here
from rxjs.
I vote unicorns. ;)
I'm unfamiliar with the fantasy-land spec, so I don't have a solid opinion other than to say it's not a priority. It doesn't seem like it's something that would be hard to add at a later point. I think we have enough on our plate trying to conform to the es-observable spec and reimplementing all of current RxJS operators.
Can we put a pin in it until those things are done?
from rxjs.
Closing this for now. Happy to discuss this at another point, though.
from rxjs.
I think everyone involved in writing Rx is familiar with Monads and their utility. Rx was one of the first βif not the firstβ popular monadic stream libraries, and was part of a family of libraries that helped educate the wider non-FP audience on the benefits of monadic composition (thanks to @headinthebox, @mattpodwysocki, @bartdesmet, @benjchristensen et. al).
To be clear, the Observable is a monad, and always has been. flatMap
is Observable's >>=
(monadic bind). The Observable also implements many other fantasy-land types, which is why I proposed interop in the first place.
I don't see much downside to Rx <-> fantasy-land interop, especially if it means more seamless integration with other popular JS libraries. What's fantasy-land's spec for unwrapping the monad (aka Observable's subscribe
)? Fantasy-land interop isn't possible unless it specifies something compatible with Rx's Observer grammar (as described in the Rx Design Guidelines [PDF]).
from rxjs.
Sorry @trxcllnt, I never meant to imply Rx contributors didn't know what a Monad was or its applicability ( I assumed you would given the library). I just wanted to make sure anyone reading this who comes from a non functional background wasn't left behind.
To be clear, the Observable is a monad, and always has been. flatMap is Observable's >>= (monadic bind). The Observable also implements many other fantasy-land types, which is why I proposed interop in the first place.
To avoid debates about what is and isn't a monad, for the sake of this discussion let's define a Monad as a Fantasy Land compliant Monad.
Here are some aspects of the current API that prevent it from being compliant.
From the docs for flatMap/selectMany
Projects each element of an observable sequence or Promise to an observable sequence, invokes the result selector for the source element and each of the corresponding inner sequence's elements, and merges the results into one observable sequence.
Emphasis mine. A Fantasy Land compliant chain method ( known as flatMap in Rx) wouldn't unwrap a promise or array specifically. The current method would stay exactly the same, but there would be a simpler chain
method that only accepts functions that return Rx Observables. If you pass in anything else it will throw an exception.
This gives us fine grained control when composing different Monads and Applicatives. We can explicitly control what is unwrapped, and when. So if you want to use an Either/Maybe/Validation for error handling instead of the built in error semantics of Observables, you can.
Arguments
selector (Function | Iterable | Promise)
selectMany
accepts Iterables and Promises as well as functions. fantasy-land/chain
would only accept a function that returns a Monad, if it received something that wasn't a function, it would thrown an error.
I imagine we can just pull the simple case into its own function and then in the existing flatMap
we can just dispatch to that when the input is a function. So pretty simple to implement.
But in order to support the Monadic spec, Rx would also need to support the Applicative spec (map
and ap
). And that would be all we'd need to do to support Fantasy Land.
I could submit a PR with tests so we can have something to look at and discuss further. I think we can achieve this with a minimal changeset.
What's fantasy-land's spec for unwrapping the monad (aka Observable's subscribe)?
That is left up to the library. Some libraries (like most) use a fork
method, others use subscribe
. Subscription is beyond the scope of the spec. All Fantasy Land is concerned with is the pure parts of the library (map, ap, of, chain etc)
from rxjs.
@JAForbes RxJS v4's flatMap
is overloaded to behave like flatMap
and flatMapTo
, while v5's isn't, but both are already compliant with the fantasy-land/chain
signature (aka (m<a>, f<a> => m<b>) => m<b>
). It also seems like Observable also already implements the Applicative spec (via the Observable.of
static method) and Apply's ap
seems like an eager specialization of join
over functions.
It doesn't make sense to implement flatMap
in terms of of
and ap
(for performance reasons), and any operations that eagerly evaluate the computation don't really fall within the purview of the Observable specification.
I want to reiterate, we can interop withfantasy-land
monads, but only if there's a specified way of unwrapping them. Implementing the Symbol.observable
spec would be good enough for us.
from rxjs.
@JAForbes RxJS v4's flatMap is overloaded to behave like flatMap and flatMapTo, while v5's isn't, but both are already compliant with the fantasy-land/chain signature (aka
(m<a>, f<a> => m<b>) => m<b>
). It also seems like Observable also already implements the Applicative spec (via the Observable.of static method) and Apply's ap seems like an eager specialization of join over functions.
So v4 is technically non compliant because you can return a Promise in the body of the transform function
- f must be a function which returns a value β
- If f is not a function, the behaviour of chain is unspecified. β
- f must return a value of the same Chain β
- chain must return a value of the same Chain β
ap
isn't eager. That is up to the particular Applicative. It isn't eager in flyd/most, it is defining a lazy consistent relationship in the form of an Observable, just like map
or flatMap
.
So perhaps join and ap are equatable, I'll need to read the docs a bit to verify that.
It doesn't make sense to implement flatMap in terms of of and ap (for performance reasons), and any operations that eagerly evaluate the computation don't really fall within the purview of the Observable specification.
There is no need to implement the actual source code using ap. Few libraries do. Its simply important that each chain is derivable in terms of ap
. In other words, it obeys the laws above.
I want to reiterate, we can interop with fantasy-land monads, but only if there's a specified way of unwrapping them. Implementing the Symbol.observable spec would be good enough for us.
Can you unwrap what you mean by unwrap π ?
If you mean, how does the fantasy land spec define subscribe
, then I already said - it doesn't. Fantasy Land leaves that up to the particular library. That particular area of a library is always impure and specific to the data structure.
from rxjs.
@trxcllnt Just reflecting on what you might mean by unwrapping. Apologies if I am completely missing what you are saying. But I think we are coming at this from different angles.
Do you think that the Observable will be wrapped in some type and you need Symbol.observable to detect if it is an observable in order to do casting? Because that isn't the case. We would just be adding some extra methods to the Observable prototype. So Observables would continue to use subscribe. And any internal implementation details in Rx would continue to stay exactly the same. No existing methods would change their behaviour or signature.
If that isn't what you are saying, maybe you want to ensure that chain returns only Observables and not other types of Chains. You could do that check if you like using whatever proprietary Rx specific code you'd like. Or, you can not do it at all. All that really matters is that chain doesn't unwrap Promises / Arrays / anything other than Observables, and that all the other laws are satisfied. Internal implementations can vary.
Maybe what you are saying is based on the assumption that ap
is eager, and therefore you'd need someway to trigger a subscription? But ap
isn't eager (or at least, there is no need for it to be eager). The internals would probably just dispatch to something like combineLatest
and return a new observable.
from rxjs.
@JAForbes re: unwrapping. Arrays have forEach
, Observables have subscribe
, the Reader monad has runReader
, Writer has runWriter
etc. When someone returns a fantasy-land/chain
monad from their flatMap
selector, what are we supposed to do with it? When someone wraps a fantasy-land/chain
monad in Observable.from()
, then subscribes to the Observable, how are we to extract the value from the fantasy-land/chain
instance?
So v4 is technically non compliant because you can return a Promise in the body of the transform function
- f must be a function which returns a value β
- If f is not a function, the behaviour of chain is unspecified. β
- f must return a value of the same Chain β
- chain must return a value of the same Chain β
Yes, then I suppose v5 is as well.
Haskell's strict typing enforces the bind functor types are consistent, so it's impossible to call Writer's bind with a function that returns a Reader monad. We can't take advantage of such guarantees, and must essentially white-list the types we interop with. If fantasy-land wants to argue that we should be stricter about the types we accept returned from flatMap
selector functions, that's a defensible position, but that battle was fought and decisively lost long ago.
from rxjs.
When someone wraps a fantasy-land/chain monad in Observable.from(), then subscribes to the Observable, how are we to extract the value from the fantasy-land/chain instance?
There is no fantasy-land/chain instance, only Rx Observables.
You would never pass another type of Monad (e.g. a Future or a Maybe) into Observable.from
. Fantasy Land is assuming an Observable instance already exists, and fantasy-land/chain
and fantasy-land/map
are just some methods that exist on the Observable prototype.
You can use Observable.from's existing api, it can consume promises/arrays whatever you want. as long as it returns an Observable
everything is fine. from
isn't part of the spec.
All that matters is that the prefixed map
, chain
, ap
and of
are consistent with the specification. And these are just new methods on the Observable prototype.
If fantasy-land wants to argue that we should be stricter about the types we accept returned from flatMap selector functions, that's a defensible position, but that battle was fought and decisively lost long ago.
Fantasy Land is not prescribing that at all (There are some interesting libraries that do, but its not part of the spec).
There are 2 valid approaches to writing chain
- You don't do any white listing, instead you dispatch assuming the types are correct, let it crash Erlang style
- Check specifically for an Rx Observable, if it isn't the correct type, throw an error, if it is, continue
The spec doesn't define whether or not you throw an error, but it does expect you will not unwrap a Promise automatically ( as an example )
- f must be a function which returns a value
- If f is not a function, the behaviour of chain is unspecified.
- f must return a value of the same Chain
- chain must return a value of the same Chain
The highlighted part of the spec could be translated into: "you can't write a selector that returns a Promise when using Observable::chain"
When it comes time to subscribe, you just do as you have always done. There is no new type to unwrap, you won't need to edit subscribe at all. Fantasy Land is not concerned with this aspect of Rx. The only changes will be some extra methods to the prototype that do not dispatch on type / unwrap non Observable values. It is much simpler than I think you are imagining.
I think this will all become a lot more clear if we can look at some code.
from rxjs.
As a user of both RxJS (v5) and Ramda, I cannot π this hard enough.
from rxjs.
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
from rxjs.
Related Issues (20)
- rxjs.dev very often inaccessible HOT 10
- export map means node always resolves CJS HOT 7
- RxJs ships generators instead of native async/await HOT 1
- bindCallback: resultSelector destructuring callback argument if it's array HOT 1
- `every` operator sending multiple values when re-entrant HOT 1
- [email protected] has peer deps?! Oops
- ObservableInput<T> should support Thennable<T>, not PromiseLike<T> HOT 1
- Delay, timer and possibly other operators have a maximum milliseconds value, even when date is passsed HOT 1
- lost stacktrace after promise is resolved HOT 1
- Teardown and error/complete messages are out of order HOT 2
- Issue with catchError HOT 1
- Synchronous emit to `switchMap` during the processing of previous emit prevents the first from unsubscribing HOT 4
- False positive in deprecation with mergMap HOT 1
- it's possible to return an array in catchError() HOT 2
- tap-complete not called for last observable inside zip HOT 2
- Interface SchedulerLike not exported from index file
- distintUntilChanged: new overload missing in bundle HOT 2
- pluck operator is a standard operator it is also available in Lodash and Ramda - It belongs into the library!
- Simplify the Observable pipe with the help of Typescript Variadic Functions HOT 3
- iif subscriptions HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. πππ
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google β€οΈ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from rxjs.