Someone pointed out the other day that this (kind of operation) is basically a syntax sugar for the ‘continuation monad’, which is the most general purpose monad (abstracting over callbacks). And that makes a lot of sense. Further reading: https://blog.poisson.chat/posts/2019-10-26-reasonable-continuations.html
Yep. When I realized this I was so weirded out I went on the Gleam discord and asked “is… is this on purpose? You guys know what you’ve done, right? Are you going to use this for general coroutines or something?” The answer was basically “yeah we know, it’ll probably get used for other things someday but for now we’re mostly just interested in error handling”.
It’s a clever design. The reason why I even thought to compare it to the continuation monad is because someone in the OCaml Discord pointed it out. The equivalent in OCaml is something like, let ( let@ ) = ( @@ ), and then you can do eg let@ channel = In_channel.with_open_bin "file.txt" in ...
Oh OK. I think a ‘do’ expression is restricted to working with the monadic bind operation though (and applicative fmap if you are using the extension). The ‘continuation monad’ syntax I am talking about is a general-purpose one that can work with any callback-style API. Another example is here https://lobste.rs/s/mmje1n/using_use_gleam#c_tvvzer
Gleam “use” syntax is very powerful and an example of great language design, a single language feature instead of many specialized features (async-await, generator, question-mark-operator, single monad do-expression, …)
This is just like Roc’s backpassing syntax, which Richard said they’re probably removing (from a submission a couple months ago) in favor of the neater but less general “!” syntax. I didn’t realize Gleam had it too. I appreciate features like this that help avoid rightward drift of code.
Would this cover the await syntax transformation too? Suppose I had something like a Promise and wanted to chain together .then calls (with rejects automatically bailing early).
(Obviously you’d need to use the results of any blocks within the function, too, as well as the function result (in this example functions are “colored”))
Would this cover the await syntax transformation too? Suppose I had something like a Promise and wanted to chain together .then calls (with rejects automatically bailing early).
Yep! You can think of use as a generalization of the await transformation. When targeting JS, promise.await takes a callback as its last argument so you can use it like so
use user <- promise.await(get_user())
use posts <- promise.await(get_posts(user))
display_posts(posts)
Much like await in JS, the return value of this block would itself be a promise.
I’m still torn on whether or not I think use is worth the cognitive load. While Gleam technically doesn’t support early return, it means that for any line like use ... <- my_fn(), my_fncan do an early return (effectively) if it so chooses–but that logic is always buried inside my_fn. But many instances of use don’t function that way–they will always (practically) call the callback. Perhaps it’s just an adjustment period for me as it’s clearly a very useful feature.
Is the rest of the catify_with_use function treated as part of the loop body? With the list.map(...) syntax, I could be certain that fn(string) would be executed once per iteration. With the use string <- list.map(strings) syntax, does every following line get executed on each iteration? For example:
So calling catify_with_use(["wibble", "wobble"]) will result in "hi" being printed twice, and the returned list being ["hi", "hi"] as the last expression is debug("hi") (it prints its argument then returns it).
To anticipate another question you may have: as I understand the article, to limit the scope of use you enclose it in a block. (No computer at hand, so not tested, sorry.)
fn catify_with_use(strings: List(String)) -> List(String) {
let old_possums_cats = {
use string <- list.map(strings)
string <> " cat"
}
debug("hi")
old_possums_cats
}
catify_with_use(["jellicle", "dramatical"])
// debug called once
// returns ["jellicle cats", "dramatical cats"]
When I see the map example though (which looks like an obvious “please do not do this, this is not what this is meant for”), I wonder if use’s elegance is hurting simplicity. This reminds me of Scala’s implicits, that are very powerful but can get confusing.
Someone pointed out the other day that this (kind of operation) is basically a syntax sugar for the ‘continuation monad’, which is the most general purpose monad (abstracting over callbacks). And that makes a lot of sense. Further reading: https://blog.poisson.chat/posts/2019-10-26-reasonable-continuations.html
Yep. When I realized this I was so weirded out I went on the Gleam discord and asked “is… is this on purpose? You guys know what you’ve done, right? Are you going to use this for general coroutines or something?” The answer was basically “yeah we know, it’ll probably get used for other things someday but for now we’re mostly just interested in error handling”.
It’s a clever design. The reason why I even thought to compare it to the continuation monad is because someone in the OCaml Discord pointed it out. The equivalent in OCaml is something like,
let ( let@ ) = ( @@ )
, and then you can do eglet@ channel = In_channel.with_open_bin "file.txt" in ...
It’s a very clever design! For me the jury is still out whether it’s too clever, but I look forward to the results.
Half the thread on the introduction of use expressions was how they’re equivalent to dot expressions.
Sorry, what’s a ‘dot expression’? Do you mean ‘do expression’?
I do! Damn too late to edit.
Oh OK. I think a ‘do’ expression is restricted to working with the monadic bind operation though (and applicative fmap if you are using the extension). The ‘continuation monad’ syntax I am talking about is a general-purpose one that can work with any callback-style API. Another example is here https://lobste.rs/s/mmje1n/using_use_gleam#c_tvvzer
Ahh, got it! Thank you!
Gleam “use” syntax is very powerful and an example of great language design, a single language feature instead of many specialized features (async-await, generator, question-mark-operator, single monad do-expression, …)
Would even be better if they did in the state-of-the-art way. E.g. https://github.com/dsyme/dsyme-presentations/blob/master/design-notes/ces-compared.md or also similar to how Haskell/Scala do it.
This is just like Roc’s backpassing syntax, which Richard said they’re probably removing (from a submission a couple months ago) in favor of the neater but less general “!” syntax. I didn’t realize Gleam had it too. I appreciate features like this that help avoid rightward drift of code.
It’s interesting how Roc and Gleam are going different ways. We used to have a specific
try
and then moved to the generaluse
.I think Gleam is doing it the right way. It’s a bit sad that it seems to reinvent the wheel.
Rather, look around and pick what works (and improve on it). I’m not a big F# fan, but I think they nailed it quite well: https://github.com/dsyme/dsyme-presentations/blob/master/design-notes/ces-compared.md
Would this cover the await syntax transformation too? Suppose I had something like a Promise and wanted to chain together .then calls (with rejects automatically bailing early).
(Obviously you’d need to use the results of any blocks within the function, too, as well as the function result (in this example functions are “colored”))
Yep! You can think of
use
as a generalization of the await transformation. When targeting JS, promise.await takes a callback as its last argument so you can use it like soMuch like
await
in JS, the return value of this block would itself be a promise.I’m still torn on whether or not I think
use
is worth the cognitive load. While Gleam technically doesn’t support early return, it means that for any line likeuse ... <- my_fn()
,my_fn
can do an early return (effectively) if it so chooses–but that logic is always buried insidemy_fn
. But many instances ofuse
don’t function that way–they will always (practically) call the callback. Perhaps it’s just an adjustment period for me as it’s clearly a very useful feature.Given the number of very complex features of other languages Gleam has side-stepped with one syntax sugar I think it has been a tremendous success!
The majority of
use
is for monadic APIs in which the callback is called conditionally. It’s uncommon for the callback to always be called.I think this other commenter pointing out that this is just how
await
works makes me realize this is just likely a symptom of unfamiliarityFor the first example,
Becomes…
Is the rest of the
catify_with_use
function treated as part of the loop body? With thelist.map(...)
syntax, I could be certain thatfn(string)
would be executed once per iteration. With theuse string <- list.map(strings)
syntax, does every following line get executed on each iteration? For example:Does
debug
get called every iteration now?It does yes, and if you rewrite the example to not use the
use
sugar, you can see more clearly how it’d work.So calling
catify_with_use(["wibble", "wobble"])
will result in"hi"
being printed twice, and the returned list being["hi", "hi"]
as the last expression isdebug("hi")
(it prints its argument then returns it).Ok thanks for the clarification! I’m not sure I like that, but it is an interesting language feature.
To anticipate another question you may have: as I understand the article, to limit the scope of
use
you enclose it in a block. (No computer at hand, so not tested, sorry.)That helps, thanks!
Great article ! I’m starting to learn Gleam and I’m not sure I grok
use
yet but the feature just feels beautiful :)From the Gleam docs, this is just syntactic sugar for the following. This code
is equivalent to this
the variables extracted with the “use” are just the values you would get if passing a callback as last argument.
This is neat, Gleam looks really promising!
When I see the map example though (which looks like an obvious “please do not do this, this is not what this is meant for”), I wonder if use’s elegance is hurting simplicity. This reminds me of Scala’s implicits, that are very powerful but can get confusing.
Yeah, I probably would use it for
list.map
myself. I think the conventional callback syntax is clearer in that case.