-
Notifications
You must be signed in to change notification settings - Fork 2
Description
EDIT by proposer (TheNavigateur): I'm creating a shortcut to my current conclusion to explain the current closed status of this issue: #3 (comment) and an Explanation of the decision here in the proposal itself
Hi! Thank you for starting this proposal, composition means a lot to me and many others in the community. I hope this feature lands in Javascript, but I hope it lands right. Our language community has a tendency to add additional features to simple specs in the interest of convenience that in the long run actually become inconvenient because they are complex, harder to explain, and affect future proposals which tend to have backwards compatibility with them.
We see composition everywhere, having a built in operator will enable new patterns, it makes me very excited. But we often compose without knowing we're composing, and that can lead to unnecessary friction with library and pattern interop.
A few examples of composition hidden in plain sight in Javascript
- styled components are just composition
- decorators, are just composition
- jsx itself is composition of functions hidden behind syntax
- Promise chaining, is sort of composition
- method chaining generally, can be considered informal composition
- Array::map for example is definitely composition
We compose but we do it informally, and that informality leads to incompatibilities between interfaces without justification. It leads to "special casing".
If we keep compose
as simple as possible, hopefully we can encourage the community to rally around that simplicity and receive emergent compatibility between libraries and specifications that were iterated on in complete isolation to eachother.
Composition is beautiful. It's extremely simple but enables so much power in that simplicity.
Promises are the exact opposite. The API is riddled with compromises, and these compromises continue to affect future proposals. One example being, Error handling behaviour of Observables being determined by Promises due to backwards compatibility.
I think there's some alternatives paths we could take to make interop with Promises more seamless without affecting the general simplicity of composition. (I'll write some code examples below) .
Alternatives:
- In the future, after this operator lands, have a compose spec that data types can support and this operator will defer to.
- Introduce a separate operator that is effectively
f => x => x.then(f)
- Take no language level action, because there's plenty of elegant userland solutions.
In the future, after this operator lands, have a compose spec that data types can support and this operator will defer to.
Data types can compose. That's what Promise.then
sort of approximates after all. For example Array's can compose:
[1].map(f).map(g)
Observables can compose
stream$.map(f).map(g)
Iterators can compose (despite not having language level support yet)
function map * (f, it){
for( let x of it ){
yield f(x)
}
}
// a lazy infinite sequence of squares that can be further composed
const squaredInfiniteRange = map( square, infiniteRange)
Composition is a general principle. Encoding specific support for Promises leads to a highly specific interpretation of composition that we'll constantly be appending to as we add new data types.
But what if we introduce a composition specification, similar to the iterator spec?
Promise.prototype[Symbol.compose] = function(f){
return this.then(f)
}
Even though then
isn't strictly composition, this solution at least allows other data types both in the language and in the community to interop with this new operator without language level coupling.
One confusing aspect of this proposal is, despite composition being general, we should not attempt to automatically compose different types together. E.g. Array's and Promises don't automatically compose with eachother, and they shouldn't. But this compose spec would make it trivial to make a mistake like that.
This problem is usually solved via transformers, but that is a finer point that specification proposal could tackle in it's own context.
Introduce a separate operator that is effectively f => x => x.then(f)
So if we want to compose promises, we can. We just need a util that can lift a function into the promises domain. then
allows us to do that.
const then = f => x => x.then(f)
const f = a => Promise.resolve(a + 1)
const g = a => Promise.resolve(a + 2)
const h = a => Promise.resolve(a + 3)
f +> then(g) +> then(h)
If people find they want to compose async values with functions, this solves for that. If it becomes popular, maybe a static then
function, or an operator that desugars to that function could be introduced into the language, leaving composition itself simple.
e.g. let's imagine there was an operator :x:
that desguared to our then
function (defined earlier)
f +> :g: +> :h:
As you can see, we can solve this problem using composition of specifications. Instead of bundling features into one big proposal. That's what composition itself teaches us to do.
Take no language level action, because there's plenty of elegant userland solutions.
I believe, if this proposal only supports functions, that's all we'll ever need. But I've hopefully demonstrated there's plenty of room for expanding on this spec in future versions if we find that isn't the case. I'd really like to give this specification some room to breathe, to be used in real projects and to encourage new practices to flourish in the community. I think this proposal could completely change how we think about and write Javascript, so there's a lot of reason to get this proposal right.
The previous solution is completely definable in userland. It's like adding ingredients when cooking, we can always add to the mix but we can't take away (so be especially careful when adding).
If there's one thing I want JS to get right, its composition. Its so powerful, so generic, so simple.
I implore you, and the community - for this language level formalization, let's keep it as simple as possible.
Thank you for your time.