Monad annoyance
8 points by raffomania
8 points by raffomania
I gotta admit, I dont get this. You’ve taken a simple pair of calls and made it super complicated. Just making two variables and assigning them is imo WAY more readable than any of the options presented here.
Like
const org = await getOrg(...);
const members = await getMembers(org.blah, ...);
What’s wrong with that?
Further down OP mentions https://github.com/supermacro/neverthrow, which has a Result
type with a map
method (if this Result represents a success, do this with the result, otherwise pass the error through unchanged), which has the same problem. await
works for Promises with then
methods, but not other similar APIs.
(This idea of a “generalized Promise”, which can be used for asynchronous IO but also things like Results, is what Haskell calls a monad.)
in languages with syntactic support for monads, this works ok.
C# (fiddle)
using System;
using System.Linq;
int[][] arrays = [[0,1,2], [], [0,1]];
var sizePlusIndex =
from a in arrays
let c = a.Length
from n in a
select n + c;
sizePlusIndex.ToList().ForEach(Console.WriteLine);
Haskell (playground):
import Control.Monad.Random
die :: RandomGen g => Rand g Int
die = getRandomR (1,6)
main = do
roll1 <- evalRandIO $ die
roll2 <- evalRandIO $ die
print (roll1 + roll2)
I was thinking that in C#, even without the LINQ syntax (which I dislike), you could use a .Select(...)
on the arrays
That double return thing is ugly, I can agree on that but I think you’re just choosing your syntax badly.
you can do it in one line and still stay under 120 characters even without the follow up .then line:
const result = getOrganization(name)
.then(async org => ({ org, members: await getMembers(org.id) }));
or shoot, even like this:
const result = getOrganization(name).then(org =>
getMembers(org.id).then(members => ({org, members})
));
But I would prefer making another function called getOrgAndMembers(name);
. I think this is a taste thing.
As other comments have mentioned, given that the OP deals with Promise
s, using await
and separate variables is the most natural way to go here. However, as an interesting tangent, I want to note that even when one is not using Promise
s, it is in fact still possible to have (somewhat cursed) monadic sugar syntax in JavaScript!
Concretely, suppose one wants to build an ergonomic JavaScript library offering a Result<T, U>
monad. To express a computation that combines multiple results, the obvious approach is to provide combinators such as Result.map
and so on, but if many results are used at once this leads to callback hell. The ideal solution is to provide some kind of syntax sugar that’s analogous to async/await
for Promises, or do notation in Haskell, or the ?
operator in Rust, whereby one can sequence multiple results in a linear fashion.
Using (two-way) generator functions, this is possible. Specifically, as the library author, we can implement a utility run
that accepts a user-provided generator function, representing some kind of computation, such that yield
ing a Result
short-circuits the computation in the error case and resumes with the success value otherwise.
const result = run(function* () {
const organization = yield getOrganization();
const members = yield getMembers(...);
});
Implementing run
is a fun exercise. The idea of course generalizes beyond Result
.
Effect actually implements this approach via Effect.gen, and so does neverthrow via safeTry. The catch is twofold. First, in practice, all the libraries will require you to write yield*
, not yield
, since TypeScript doesn’t infer types for yield
expressions but the related operator yield*
can be typed in the desired way. Second, since there is no generator arrow function shorthand, writing the rather wordy function* () {}
to define a generator function is required. (See this TC39 proposal for generator arrow function notation).
To be clear, I don’t really think emulating do notation using generator function is a good idea, but it’s a fun use case for a lesser-known language feature.