Giter Club home page Giter Club logo

Comments (23)

JAForbes avatar JAForbes commented on August 15, 2024 64

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 for stream.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.

JAForbes avatar JAForbes commented on August 15, 2024 56

@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])

See a live example

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.

benlesh avatar benlesh commented on August 15, 2024 32

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.

staltz avatar staltz commented on August 15, 2024 25

Just use most.js

from rxjs.

headinthebox avatar headinthebox commented on August 15, 2024 21

@trxcllnt What have you been smoking?

from rxjs.

dypsilon avatar dypsilon commented on August 15, 2024 18

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.

joneshf avatar joneshf commented on August 15, 2024 17

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.

kwijibo avatar kwijibo commented on August 15, 2024 16

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.

chasm avatar chasm commented on August 15, 2024 8

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.

trxcllnt avatar trxcllnt commented on August 15, 2024 1

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.

mattpodwysocki avatar mattpodwysocki commented on August 15, 2024

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.

mattpodwysocki avatar mattpodwysocki commented on August 15, 2024

@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.

benlesh avatar benlesh commented on August 15, 2024

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.

benlesh avatar benlesh commented on August 15, 2024

Closing this for now. Happy to discuss this at another point, though.

from rxjs.

trxcllnt avatar trxcllnt commented on August 15, 2024

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.

JAForbes avatar JAForbes commented on August 15, 2024

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.

trxcllnt avatar trxcllnt commented on August 15, 2024

@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 avatar JAForbes commented on August 15, 2024

@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.

JAForbes avatar JAForbes commented on August 15, 2024

@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.

trxcllnt avatar trxcllnt commented on August 15, 2024

@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.

JAForbes avatar JAForbes commented on August 15, 2024

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

  1. You don't do any white listing, instead you dispatch assuming the types are correct, let it crash Erlang style
  2. 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.

nateabele avatar nateabele commented on August 15, 2024

As a user of both RxJS (v5) and Ramda, I cannot πŸ‘ this hard enough.

from rxjs.

lock avatar lock commented on August 15, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.