Skip to content
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

Why there is no creed.of method? #177

Open
dmitriz opened this issue Apr 26, 2018 · 10 comments
Open

Why there is no creed.of method? #177

dmitriz opened this issue Apr 26, 2018 · 10 comments

Comments

@dmitriz
Copy link
Contributor

dmitriz commented Apr 26, 2018

The FL Applicative spec includes the of method but it does not seem to be available on creed:

> creed.of(1)
TypeError: creed.of is not a function

Any reason not to have it?

It could be mentioned that of is actually more basic than Applicative and is part of the Pointed Functor Spec, see also https://github.com/MostlyAdequate/mostly-adequate-guide-it/blob/master/ch9.md#pointy-functor-factory.

It seems that creed.fulfill is doing what of is meant to do,
which is somewhat non-standard name and is longer to write.
Also, when it is not called of, the question arises whether it conforms
to the Pointed Functor spec, which I understand it does.

If creed.fulfill is indeed intended to satisfy the Pointed Functor spec (together with map),
maybe also alias it as of and add tests for the spec?

@briancavalier
Copy link
Owner

Hi @dmitriz. The FL has changed slightly over time with regard to where of should be placed. At one time, it was required to be on the type's constructor and/or prototype.

So, creed had placed it at creed.Promise.of, which is still present:

> var { Promise } = require('creed')
undefined
> Promise.of
[Function: of]

The language has changed to "type representative". The of section as well as the last paragraph of the type representative section seem to indicate that it's still required to be named constructor. That makes it sound like any promise creed creates would still need to have p.constructor.of. That's currently also true:

> var p = Promise.of(123)
undefined
> p.constructor.of
[Function: of]
> p.constructor.of === Promise.of
true

So, I think creed still satisfies Applicative / Pointed Functor. To be honest, I haven't read the FL spec in depth in a while, so I hope I'm reading all of that correctly!

It may still be perfectly reasonable to add of as a named export alias of fulfill. On one hand, it's nice because, like you said, it matches the FL name and it's short. On the other hand, having 2 exported names for the same thing can be confusing. What are you thoughts?

@dmitriz
Copy link
Contributor Author

dmitriz commented Apr 26, 2018

Thank you @briancavalier for the explanation, I would have never guessed it is on the creed.Promise namespace. ;) It does not even show up in the REPL:

> creed
{ enableAsyncTraces: [Function: enableAsyncTraces],
  disableAsyncTraces: [Function: disableAsyncTraces],
  resolve: [Function: resolve],
  reject: [Function: reject],
  future: [Function: future],
  never: [Function: never],
  fulfill: [Function: fulfill],
  all: [Function: all],
  race: [Function: race],
  isFulfilled: [Function: isFulfilled],
  isRejected: [Function: isRejected],
  isSettled: [Function: isSettled],
  isPending: [Function: isPending],
  isNever: [Function: isNever],
  isHandled: [Function: isHandled],
  getValue: [Function: getValue],
  getReason: [Function: getReason],
  coroutine: [Function: coroutine],
  fromNode: [Function: fromNode],
  runNode: [Function: runNode$1],
  runPromise: [Function: runPromise$1],
  delay: [Function: delay],
  timeout: [Function: timeout],
  any: [Function: any],
  settle: [Function: settle],
  merge: [Function: merge],
  shim: [Function: shim],
  Promise: 
   { [Function: CreedPromise]
     resolve: [Function: resolve],
     reject: [Function: reject],
     all: [Function: all],
     race: [Function: race] } }

My personal reaction is, whether the nested namespace is really necessary and not adding any extra complexity making the library harder to use (and discover its features). I can also see another resolve there, which feels even more confusing.

Would it not be simpler to have the main library creed as the type representative with all methods defined there? That would automatically remove the double resolve problem. Then of can also be put there and declared to be identical with fulfill, which I think is very helpful to know and not confusing at all. The name of alludes to the Pointed Functor and fulfill to the actual implementation.

On the other hand, the creed.Promise can be confused with the native Promise, which still leaves me wondering about their difference, apart from having some more methods. Of course, I might be missing some point haven't used it that much.

@bergus
Copy link
Contributor

bergus commented Apr 29, 2018

@dmitriz

On the other hand, the creed.Promise can be confused with the native Promise

That's the whole idea. The Creed Promise constructor can be used as a drop-in replacement for the native promise constructor.

@dmitriz
Copy link
Contributor Author

dmitriz commented Apr 30, 2018

@bergus

That's the whole idea. The Creed Promise constructor can be used as a drop-in replacement for the native promise constructor.

Hm... Then why not just use the native Promise for the common methods? Any advantage? Any difference?

@briancavalier
Copy link
Owner

tl;dr I'm open to simplifying/streamlining the API for a 2.0 release

@dmitriz Creed was intended to help bridge between A+ / ES and FL. It was created at a time when native promises had significant performance problems, async/await wasn't easy to use everywhere, and FL was still fairly new.

So, many of the 1.x API decisions were made under quite different circumstances than exist today. It's easier to see some of the API inconsistencies with hindsight. I'm sure there's plenty of room for improvement, and I do appreciate your fresh perspective on it.

The API is intended to be used primarily as named exports, and creed.Promise is exported mostly just in case someone might ever need it. Perhaps not the best reason, but again, hindsight. of being on Promise was mostly for FL compat.

As @bergus mentioned, creed's Promise was intended as a drop in replacement for A+/ES. The shim() function will forcibly install creed's Promise as global Promise. There were (and still are, imho) advantages to that:

  1. Afaik, creed is still faster and more memory friendly than native promises.
  2. Using multiple promise implementations in a single app incurs assimilation penalties. Many promise implementations, including creed, can optimize interactions (bypassing then) with their own promises, but are forced to interact with other implementations by only using then. Thus, if your app uses explicit promise via creed, it is beneficial that other promises created via global Promise are also creed promises.
  3. Creed's async stack traces are pretty helpful.

All of that said, I'd certainly be open to simplifying/streamlining the API for a 2.0 release.

@unscriptable
Copy link
Contributor

  1. Creed's async stack traces are pretty helpful.

Correction: Creed's async stack traces are awesome!

@dmitriz
Copy link
Contributor Author

dmitriz commented May 4, 2018

@briancavalier
Many thanks for your detailed explanations, greatly appreciated!

I can see the benefits for both replacing the native Promise,
as well as of some more "lighter" way to use creed in addition to the native promises.

All of that said, I'd certainly be open to simplifying/streamlining the API for a 2.0 release.

A big selling point of creed I could see, is to use the proper functional
operators like map that are lacking for the native promises.
A very lightweight way to use it, with as little other changes to the code as possible,
could be to follow the Static Land's Functor Spec as

creed.map(someFunction, nativePromise)

which would return what I would call a "creed promise",
as opposed to the JS native promise.
The latter would be the wrong type to return here
because e.g. it does not allow to wrap promise into promise.

Now I understand that creed does not aim to be SL-compatible (which would be nice!),
looking for FL instead. The problem with the FL-compatibility though,
it does not seem to provide similar lightweight ways to use libraries, if I understand it correctly.
Instead of the point-free (aka point-less 😄 ) style, it requires to use methods,
that I somehow need to get first for my nativePromise instance.
This makes it more verbose with at least two new operations instead of one.
Also the instance methods could be more invasive than static ones and harder to use
in functional composition pipelines.

My guess is that the currently intended way is

creed.resolve(nativePromise).map(someFunction)

which does not feel as obvious and straightforward,
due to the non-standard creed.resolve method
that is easy to confuse with Promise.resolve.
If I understand correctly, the latter will unwrap
any nested promise, whereas the former will not,
making it for a subtle difference
I would need to delve into,
where all I wanted was really to get my map working. ;)

Perhaps something like

let creedPromise = creed.fromPromise(nativePromise)

could be a lightweight way of cooking up a "creed promise"
that would be obvious to any outsider?

Another suggestion, since the native promise is by now established,
perhaps call this flavour a "Creed Promise", as a matter of terminology.
That would make it easy to be used in explanations along with native promises,
as in the above examples.

@briancavalier
Copy link
Owner

Again, there is history: Static land didn't exist (or at least, I wasn't aware of it) when creed was created, and FL did exist, so picking a "standard" on which to base the API was easy 😄 . Also, tree-shaking wasn't practical and/or widely used, and function composition (which will tend to require partial application) as a programming model in JS wasn't prevalent.

Since then, I decided to implement both SL and "functions-mostly" in another project, and then eventually transition to functions-only. I've been watching this closely to see when the time is right to adopt it. I might rather go in that direction, once it gets a bit more consensus, but it seems to leave the tree-shaking question open.

Methods aren't all bad: they allow typeclass-like dispatching, whereas functions require either using switch statements or passing around typeclass dictionaries to achieve the same, both of which can be cumbersome. IIUC, the latter is one reason FL chose methods as its primary typeclass representation.

Creed's design takes advantage of that to provide optimized implementations for fulfilled, rejected, and never promises. I've found that representing some small-ish number of core low-level operations via methods plus a public API via functions works quite well. Creed simply uses methods where there was opportunity for different promise variants to provide a specialized implementation.

It would be an interesting research project to see how shifting more toward functions compares, both readability/maintainability-wise and the ability to optimize cases like fulfilled, rejected, and never.

@briancavalier
Copy link
Owner

I like being explicit with fromPromise (this also matches some other JS projects). I have also come to prefer just as point, since there are variadic uses of of in the JS ecosystem that make it confusing (e.g. Array.of, Observable.of). However, it seems likely that the unified FL would adopt of. Either is ultimately fine with me.

If I understand correctly, the latter will unwrap
any nested promise, whereas the former will not,

creed.resolve and creed.Promise.resolve are the same function.

Another suggestion, since the native promise is by now established,
perhaps call this flavour a "Creed Promise", as a matter of terminology.

Yes, that seems helpful now that "promise" tends to mean ES Promise.

@dmitriz
Copy link
Contributor Author

dmitriz commented May 7, 2018

@briancavalier

Again, there is history: Static land didn't exist (or at least, I wasn't aware of it) when creed was created, and FL did exist, so picking a "standard" on which to base the API was easy 😄 .
Also, tree-shaking wasn't practical and/or widely used, and function composition (which will tend to require partial application) as a programming model in JS wasn't prevalent.

Aha, interesting to know, thanks!
The number of standards is not as small anymore these days,
e.g. Jabz prefers combine over concat
(that I tend to agree with as concat deals with the more special free monoid),
also flatMap is used by several libraries instead of chain,
which is less confusing (there are other chains in JS) and consistent with Scala,
and generally more descriptive and intuitive to folks outside FP.

Since then, I decided to implement both SL and "functions-mostly" in another project, and then eventually transition to functions-only. I've been watching this closely to see when the time is right to adopt it. I might rather go in that direction, once it gets a bit more consensus, but it seems to leave the tree-shaking question open.

That looks very interesting.

Methods aren't all bad: they allow typeclass-like dispatching, whereas functions require either using switch statements or passing around typeclass dictionaries to achieve the same, both of which can be cumbersome. IIUC, the latter is one reason FL chose methods as its primary typeclass representation.

I find both useful to have.
Methods are hard to beat when writing something like

promise.map(f).map(g).flatMap(res => doSmth(a, res))

whereas functions can be nicer to use with non-native but conforming promises

pipe(map(f), map(g), flatMap(res => doSmth(a, res)))

that you can run against any promise with the same abstract code,
as long as the first map can pick it up.

Creed's design takes advantage of that to provide optimized implementations for fulfilled, rejected, and never promises. I've found that representing some small-ish number of core low-level operations via methods plus a public API via functions works quite well. Creed simply uses methods where there was opportunity for different promise variants to provide a specialized implementation.

That makes perfect sense.
It is probably best to keep the core as small as possible,
with choices made for mostly optimisation-motivated reasons,
and external plugins providing aliasing or more user-friendly api.

It would be an interesting research project to see how shifting more toward functions compares, both readability/maintainability-wise and the ability to optimize cases like fulfilled, rejected, and never.

This discussion for flyd can be relevant, basically, the best of both worlds seems to be the best :)
paldepind/flyd#137

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants