Closed yorickpeterse closed 3 months ago
Moving closures introduce a caveat: if you want to move an owned value out of a closure, you need to use fn move
to capture by moving. In practice, this means that every time you want to capture an owned value and move it out of the closure, you have to use fn move
, only using a bare fn
if you're moving borrows at which point you might as well use a normal closure.
From the perspective of somebody calling a closure, there's also not a clear benefit to only being able to call the closure once. That is, methods such as Option.then
work just fine without a moving closure (as in, it doesn't matter to them how many times you can call the closure).
Another note: fn move
currently results in all captures being captured by moving. But what happens if we want to borrow captures A and B, and only move C? Right now there's no way of doing that.
One option is to make the list of captured variables explicit, i.e. something like this:
fn (arg1, arg2) move (capture1) { capture1.do_something(capture2) }
Here capture1
would be captured by moving, while any other captures are captured by borrowing. This removes the need for doing this instead (a pattern not uncommon in Rust):
let capture2 = ref capture2
fn move (arg1, arg2) { capture1.do_something(capture2) }
Another option is this: if a closure moves some variable A
, it's inferred as a moving closure and captures the variable as an owned value instead of a borrow. The problem now is that if we want to actually borrow it (e.g. pass a borrow of it to a generic argument T
), we need a way to explicitly signal that.
To put it differently, detecting whether a variable should be captured as a borrow or owned value is going to be tricky/prone to error.
I'm going to leave this be for the time being. Introducing one-shot closures introduces several challenges/complications, both for the compiler and the user. For example, for a one-shot closure to work it must capture variables by moving them. This means that a one-shot closure is always fn move
but an fn move
isn't necessarily a one-shot closure, which is confusing.
In addition, our current "all or nothing" approach to capturing means that you can't have a one-shot closure that borrows certain values while taking ownership of others, instead they're either all borrowed or moved. This will make working with one-shot closures more difficult, resulting in a pattern similar to Rust as described in https://github.com/inko-lang/inko/issues/347#issuecomment-2263515315.
For cases where we have nested closures, such as the Option.then
example, I think we're better off with dedicated methods to solve those issues, rather than introducing more complexity in the type system. One can also use a regular match
expression if necessary.
If at some point in the future it's deemed necessary to have one-shot closures, we can revisit this.
During the work on https://gitlab.com/inko-lang/inko/-/merge_requests/120, Inko briefly supported closures that could be called at most once, using the syntax
fn once { ... }
. This was later removed as I wasn't sure about their value.Recently I've been spending time writing a small parsing combinator framework, mostly as a way of testing Inko's type system and to see what functionality might be missing. Several times I ran into cases where one-shot closures would be necessary. The simplest example is when you have
Option
A andOption
B, and you want to map them into(A, B)
like so:This won't work because then the
b.then
closure captureslhs
, it exposes it asmut A
, meaning we end up with anOption[(mut A, B)]
instead ofOption[(A, B)]
.There are probably other cases where one-shot closures are useful. In fact, I think that if we were to reintroduce them, we should recommend using them by default unless you want to call a closure multiple times.
Syntax
I prefer not to introduce the
once
keyword for just this case, but it is the most obvious/clear syntax:I'm also not sure yet how to combine this with
fn move
: do we usefn once move
orfn move once
? Or doesfn once
simply implyfn move
?Another option is to use
fn!
instead of justfn
, but this syntax isn't self-explanatory.Semantics
A one-shot closure's
call
method is generated asfn move
, instead offn mut
. The captured variables are exposed using their original types (i.e. a capturedT
is exposed asT
, notmut T
). Anfn
isn't compatible withfn once
and vice-versa.Tasks