Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Should we conform to the fantasy-land specification? #34
@trxcllnt that's not really compelling per se if Observable is indeed a standard. We'll support in
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?
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.
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!
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:
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.
@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
Of course, you wouldn't need to change the actual
Here is a quick one:
You might be familiar with
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
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
You can benefit from this stuff without needing to know what a Monad is.
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.
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.
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
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 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
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
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.
I imagine we can just pull the simple case into its own function and then in the existing
But in order to support the Monadic spec, Rx would also need to support the Applicative spec (
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.
That is left up to the library. Some libraries (like most) use a
@JAForbes RxJS v4's
It doesn't make sense to implement
I want to reiterate, we can interop with
So v4 is technically non compliant because you can return a Promise in the body of the transform function
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
Can you unwrap what you mean by unwrap
If you mean, how does the fantasy land spec define
@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
@JAForbes re: unwrapping. Arrays have
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
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
You can use Observable.from's existing api, it can consume promises/arrays whatever you want. as long as it returns an
All that matters is that the prefixed
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
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 )
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.
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
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
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
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
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:
Fantasy Land is instead concerned with this kind of interoperability:
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:
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
(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)
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.