5 ms·
I've spent some time thinking about language design recently. Coloured functions have a surprising number of downsides: - They're sometimes much too explicit.
by fleabitdev 2y ago
I've spent some time thinking about language design recently. Coloured functions have a surprising number of downsides:
- They're sometimes much too explicit. When writing a complicated generator, I don't necessarily want to annotate every call to a sub-generator with `yield*`, especially if I need to drill that annotation through wrapper functions which aren't really generators themselves.
- Colours show up everywhere. If you have several functions with a `context: GodObject` parameter, then that's a function colour. Most third-party code will be unable to forward a `context` argument to a callback, so you'll have to manually smuggle it in using closures instead.
- Different "colour channels" don't compose nicely with one another. Even though JavaScript ES2017 provided both async functions and generators, ES2018 had to add multiple new pieces of syntax to permit async generators.
- It's normally impossible to write code which is generic over a function's colours. For example, if you have `function callTwice(f)` in JavaScript, you'd need a separate `async function callTwiceAsync(f)`, and a `function* iterateTwice(iterable)`, and an `async function* iterateTwiceAsync(iterable)`, despite the fact that all of those functions are doing the same thing.
Several small languages are experimenting with algebraic effect systems [1], which would make all functions colourless. If JavaScript had this feature, the syntax for defining and calling functions would be the same, no matter whether you're dealing with a normal function, a generator, or an async function. No more `async`, `await`, `function*`, `yield`, `yield*`, `for-of`, `for await`...
This can make everything too implicit and vague. The state of the art is for algebraic effect systems to be implemented in a statically-typed language, which then uses type inference to quietly tag all function types with their colours. This means that the compiler can enforce requirements like "you can only call generators within a scope which is prepared to collect their results", but that rule wouldn't prevent you from wrapping a generator in `callTwice`.
[1]: https://github.com/ocaml-multicore/ocaml-effects-tutorial https://github.com/ocaml-multicore/ocaml-effects-tutorial
- magicalhippo 2y ago> Most third-party code will be unable to forward a `context` argument to a callback Having a context argument which is passed to the callback has been par for the course in all cases I've seen, but I've mostly been in C land and related areas. Though, if you have closures, should you still prefer a context param? > If JavaScript had this feature, the syntax for defining and calling functions would be the same, no matter whether you're dealing with a normal function, a generator, or an async function. How would one reason about performance and possibly concurrency in such languages?
- fleabitdev 2y agoIn a language with algebraic effects, questions like "how should I make this `CancellationToken` available to these forty small functions?" simply go away :-) It's ambient state, just like a `catch` block, and so you can make it ambiently available to a whole tree of function calls. The outcome is the same as passing around a context argument by hand, and it can have equally strong type-checking, but the compiler handles all of the boilerplate for you. To answer your second question: I agree that, in a dynamic language like JavaScript, making all of these language features implicit could be too costly. I'm more optimistic about effect systems in statically-typed languages.
- magicalhippo 2y ago> ambiently available to a whole tree of function calls So a "localized global variable" of sorts? Sounds interesting, got any links to examples I could look at?
- fleabitdev 2y agoThe term is "dynamic scope": https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scope_vs._dynamic_scope https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexic... It isn't a very common language feature, but you could take a look at React's `Context` or Common Lisp's `defparameter`.
- magicalhippo 2y agoAh, like Python objects then. Which is unfortunate, because that's one of my main dislikes with languages like Python. I was imagining more along the lines of (on-the-spot pseudo-code) using [x := foo(...)] { bar(); output(); } func bar() { x = x + 1 } where bar() and output(), or something they call in turn, can reference x and it would be a compile-time error to use bar() or output() in cases where they couldn't resolve x. Or something more along those lines. Anyway, cheers, always fun to ponder these things.