-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fantasy land update #166
Fantasy land update #166
Conversation
2 similar comments
I'm not sure what the consensus is on deprecating Migration will be as simple as: - stream(promise)
+ flyd.fromPromise(promise) |
Does this include promise handling in |
@StreetStrider ah, forgot about that. Yes, I'm talking about deprecating the whole shebang. The benefit of the promise swallowing mechanic has always eluded me. Since this pr makes stream monads I don't see how promise swallowing is of value any more. Consider: const {stream} = require('flyd');
const getResults = filter => requestPromise('https://example.com/search?q=' + filter);
const filter$ = stream('');
const result$ = filter$.map(getResults) What you have there is a With the features added in this PR const {stream, chain, fromPromise} = require('flyd');
const getResults = filter => requestPromise('https://example.com/search?q=' + filter);
const filter$ = stream('');
const result$ = filter$
.pipe(chain(f => fromPromise(getResults(f))));
// or
const result$ = filter$
.pipe(map(getResults))
.pipe(chain(fromPromise)) Furthermore the fantasy-land spec explicitly says for Applicatives:
So we can't have a fantasy-land monad without either
In my mind option 1. is the better one, it involves less gotchas, and less magic. |
@nordfjord hello.
So, before, for me this feature is a good way to handle promises in simple scenarios without diving into the whole fantasy-land business. Sad, but if it blocks implementation of this specs, it should be removed, especially considering some magic behind it, corresponding to my notes above. |
I see what you’re saying. I’m going to give a couple of observations WRT streams and promises.
To properly handle all cases I have used this helper function before function fromPromise(p) {
const s = stream({state: “loading”});
p
.then(value => ({state “resolved”, value}))
.catch(error => ({state: “rejected”, error})
.then(s)
.then(()=> s.end(true);
return s;
}
|
lib/index.js
Outdated
if (arguments.length > 0) s(initialValue); | ||
return s; | ||
} | ||
// fantasy-land Applicative | ||
flyd.of = flyd.stream; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these two lines necessary? They're not required for FL and flyd.stream
does the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, will remove
- stream(promise)
+ flyd.fromPromise(promise) Not in all cases as far as I can see. For instance this example streamX
.map(doSomeAsyncStuff)
.map(doMoreAsyncStuff)
.map((result) => result.done ? true : doEvenMoreAsyncStuff(result.workLeft)); The basic idea behind promise shallowing is based on what @StreetStrider wrote:
It can bring some nice simplifications to just handle promises as "delayed" pushes to the child streams. Additionally, the implementation of the feature is very simple. I think the removal of the inbuilt handling of promises should be discussed in a separate issue. The rest of this PR are really great improvements and they're much less "controversial". |
Very well, I'll remove the deprecation. I'll open up a separate issue to discuss the promise handling mechanics. |
82c71da
to
5afb43b
Compare
Ok, I've removed the promise swallowing deprecation. But I've also had to remove the line in the readme that says flyd implements the fantasy-land monad specification. Because it doesn't, because of promise swallowing. |
I've also opened a seperate issue to discuss promise swallowing. #167 |
Considering the above example // this function returns Promise, which is not FL compliant
const getResults = filter => requestPromise('https://example.com/search?q=' + filter);
const result$ = filter$
.pipe(map(getResults))
.pipe(chain(fromPromise)) // chaining non-FL-compliant function Would the following syntax not be simpler and FL-compliant: // convert to FL-compliant stream right away
// a -> Stream b
const getResultsStream = filter => fromPromise(
requestPromise('https://example.com/search?q=' + filter)
)
// Get Stream b by chaining Stream a ~> (a -> Stream b) -> Stream b
const result$ = filter$
// use the ordinary monadic `chain` on Streams
.chain(getResultsStream) |
@nordfjord Since this PR proposes to use |
@dmitriz you can get that behaviour by doing: const FL = require('fantasy-land');
FL.map = 'map';
FL.chain = 'chain';
FL.ap = 'ap';
FL.of = 'of'; or: Object.assign(FL, Object.keys(FL).reduce((p,k)=> {
p[k] = k;
return p;
}, {})); In your own code |
@nordfjord You mean, mutating |
Yes, to support the fantasy-land spec we In the flyd code we do: s[FL.map] = boundMap; So if you mutate |
An example: import * as FL from 'fantasy-land';
FL.chain = 'chain';
import * as flyd from 'flyd';
const s = flyd.stream(1)
.chain(()=> flyd.stream(2));
s() // 2; |
README.md
Outdated
@@ -426,6 +424,57 @@ __Example__ | |||
var numbers = flyd.stream(0); | |||
var squaredNumbers = flyd.map(function(n) { return n*n; }, numbers); | |||
``` | |||
### flyd.chain(fn, s) | |||
Given a stream of streams, returns a single stream of merged values from the created streams. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't s
here just Stream a
rather than stream of streams?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's true, I copied this from that flatMap
readme, I'll fix
@@ -426,6 +424,57 @@ __Example__ | |||
var numbers = flyd.stream(0); | |||
var squaredNumbers = flyd.map(function(n) { return n*n; }, numbers); | |||
``` | |||
### flyd.chain(fn, s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the FL-standard s.chain(fn)
or the static chain(fn)(s)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about them? can you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whether I can use them instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have never seen the syntax like here in this context. Any motivation for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's actually no FL-standard s.chain(fn)
.
If you read the spec, there's a requirement for a prefixed fantasy-land/chain
to comply with the spec.
flyd.chain
can be used in a number of ways:
- directly
flyd.chain(fn, s);
- through pipe
s.pipe(flyd.chain(fn));
- bound to the stream as
fantasy-land/chain
s['fantasy-land/chain'](fn);
All of these ways are functionally equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean here:
https://github.com/fantasyland/fantasy-land#chain-method
m.chain(f)
As a monad, flyd.stream
object has to implement the .chain
method.
I didn't mean the prefixed methods.
They are in addition, more for the library creators and the situation
when you have low level libraries used by higher level ones.
However, there is no higher level library for flyd
.
I can see the other ways, but none of them is really as usable,
they are more for a low level library.
So I would need to write a micro-wrapper of my own,
which I shall do if the direct methods won't be supported out of the box,
but I've thought it might be useful for more people than just me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the part of the spec I linked says:
Further in this document unprefixed names are used just to reduce noise.
Meaning it's the prefixed methods that matter. Unprefixed methods are not part of the spec and as such can for the most part be treated as having undefined behaviour.
Now we really have 2 camps when it comes to FP in JS, there's the Haskell camp, and the Scala camp.
The Scala camp prefers method chaining and the Haskell camp prefers function composition.
To me, the pipe method should make both camps happy.
The function composition camp can write:
import {stream} from 'flyd';
import {chain, map, compose, prop} from 'ramda';
const filter$ = stream('');
const result$ = compose(
map(prop('content')),
chain(compose(fromPromise, getResult))
)(filter$);
and the method chaining camp can write:
import {stream} from 'flyd';
import {chain, map, prop} from 'ramda';
const filter$ = stream('');
const result$ = filter$
.pipe(chain(compose(fromPromise, getResult)))
.pipe(map(prop('content')));
Note: chain and map could also be imported from flyd, but I wanted to show that other libraries supporting the fantasy-land spec can be used too.
@nordfjord The code is getting some implicit flavor when we are mutating what we don't see, From the usability point of view, wouldn't it be easier for most users simply enabling it by default? Otherwise I would have to maintain a wrapper doing that, |
You only need to mutate once, which you can do in something like your
The cons of providing both
Sanctuary had a similar discussion here I remember @paldepind saying somewhere he felt disgusted having to provide methods just to support the fantasy-land spec.
They would be effected in the same way. i.e. their exports would have |
There are also more comments there and arguments against these Cons, many different opinions. But my point here is merely practical. Any additional complication to the syntax will go the opposite way, The Node's Stream syntax had been notably criticised by TJ: |
Agreed. I just think Why do I think this? It provides a uniform way to interact with example: const filter = require('flyd/module/filter');
const {stream, map} = require('flyd');
const numbers = stream(1);
const evenNumbersSquared = numbers
.pipe(filter(d => d % 2 === 0))
.pipe(map(d => d * d)); Using map, and filter is now actually the same syntax. Take a look at rx-js and their decision to move to They provide some really great arguments about why
Agreed. Are we not in agreement that
I'm not familiar with TJ or why his comment is notable. I don't see how to lift his comment into this discussion. |
I see where you come from and the arguments. My highest priority is to reduce the coding and maintenance time by reducing the code's complexity. What it means in practice, my code should work universally e.g. for any Monad: let newMonad = oldMonad.chain(f) or let newMonad = chain(f)(oldMonad) Note that I consider these two expressions conceptually identical. With that principle in mind, I can freely switch between the two syntaxes, whatever makes my code more readable. And it works for any Monad, whether it is a stream or Either or anything else. So I don't need to have any separate code just to deal with streams. The problem with using In the ideal world, What is even worse, since Concerning Rx, they don't seem to embrace the functional approach, so would have different aims and priorities. For instance, their worry about tree-shaking is more acute for them simply because of the (enormous) size of their library. Having said that, I still find it great what you are doing here and it is ok to have different priorities. |
I agree, I'm not explicitly against having these methods unprefixed as well. Let's get @paldepind's input on this. |
Yes, you're right. When I implemented the promise handling I didn't consider how it would affect the implementation of the "standard" structures. But with promise swallowing a Flyd stream is, strictly speaking, not even a functor. I'll have more to say about that in the other thread.
Yeah, I remember saying something like that when prefixing the FL methods was discussed in FL. I've since changed my opinions. I used to think that methods were "non-functional" and I tried to avoid them. These days I consider methods to be just a form of functions with some different properties (the syntax for invoking them is different, they're namespaced on the object) that gives different pros and cons. What matters is whether the function or method is pure. If it's pure it's functional enough for me 😄 Currently methods compose in a nice way syntactically when using chaining. That gives them an advantage to functions. I'm looking forward to the pipe operator changing that. Hi dmitriz. Long time, no chat. Nice to see you, I hope you're doing well 😄
let newMonad = oldMonad.chain(f)
let newMonad = chain(f)(oldMonad)
I think you raise some very good points. Using Now, there's nothing that prevents Flyd from having unprefixed methods as well. I think doing that would be fine. But those can only be there for convenience and not for writing universal monad code. |
index.d.ts
Outdated
|
||
pipe<V>(operator: (input: Stream<T>) => Stream<V>): Stream<V>; | ||
|
||
['fantasy-land/map']<V>(accessor: Morphism<T, V>): Stream<V>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason for using Morphism
instead of just (a: A) => B
? I think the latter would be a bit more "mainstream"? Maybe even more idiomatic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No particular reason. Leftovers from other typings in my project.
I think accessor is the wrong name for the argument as well. How about project
?
I think exactly the same way. As for Instead one should register In practical sense it creates additional work for programmer to implement compability, so people still using naive implementations all around. I'm sure this was discussed somewhere in FL or elsewhere. |
Hi @paldepind
Thanks, good to hear from you too.
I see that my interpretation of the FL compatibility is outdated :( I can see the |
I've removed the deprecations of I've also added I did leave the old behaviour of After our discussion in #167 I've added a deprecation notice to Promise swallowing and opened #168 to remove promise swallowing (that PR depends on this one) I think the best way moving forward is to Remove promise swallowing in one fell swoop with no intermediate release, because the helper methods we want to provide I none the less wanted to keep the door open for an intermediate release should you desire that, so I've made it seperate PRs for that reason. |
Great! This is now on npm 🎉 Now it's time to break things. |
Break things (followup from #166)
This PR:
pipe
method to ease the transition from old to new fantasy-land for end usersThe PR also exposes
chain
,of
, andap
as methods on theflyd
object. This is intended for them to be as operators inpipe
Before I consider this complete I will need to:
Any help with the above will be greatly appreciated