Kotlin like function call with closure

Summary

In Kotlin we can do something like that:

fun transform(toTransform: Int, transformer: (Int) -> String): String {
   return transformer(toTransform) 
}

And then call it:

transform(10) { int -> int.toString() } // We can name first parameter
transform(10) { it.toString() } // Or use as it

What if add this feature of Kotlin to Rust? The syntax example:

fn transform<F>(to_transform: u32, transoform: F) -> String
   where F: FnOnce(u32) -> String {
   transform(to_transform)
}

transform(10) { |int| int.parse().unwrap() };
transform(10) { it.parse().unwrap() }

There is we call function with two arguments: number to transform and closure, but don't parenthesize the last one.

Also in Kotlin we can call closure with receiver(it's self in Rust) and than access it as "this":

fun transformAsReceiver(toTransform: Int, transformer: Int.() -> String): String {
  return transformer(toTransform)
}

transformAsReceiver(10) { this.toString() }

In Rust it'll be like:

fn transformAsReceiver<F>(to_transform: u32, transformer: F) -> String 
  where F: FnOnce(u32 as self) -> String {
 transformer(to_transform)  
}
transformAsReceiver(10) { self.parse().unwrap() }

Motivation

More readable DSL

This syntax sugar will be useful for builder pattern to create easy to read DSL without any macros e.g. Kotlin's Ktor web framework uses it for HTML and routing:

fun Application.module() {
    routing {
        get("/") {
            val name = "Ktor"
            call.respondHtml(HttpStatusCode.OK) {
                head {
                    title {
                        +name
                    }
                }
                body {
                    h1 {
                        +"Hello from $name!"
                    }
                }
            }
        }
    }
}

There is example how GUI definition with egui will be look like:

let options = eframe::NativeOptions { 
   initial_window_size: Some(egui::vec2(320.0, 240.0)), 
   ..Default::default() 
}; 
  
// Our application state: 
let mut name = "Arthur".to_owned(); 
let mut age = 42;

eframe::run_simple_native("My egui app") { move |context, _frame|
   egui::CentralPanel::show(context) {
      self.heading("My egui application");
      self.horizontal() {
         let name_label = ui.label("Your name: "); 
         self.text_edit_singleline(&mut name) 
             .labelled_by(name_label.id);
      };
      self.add(egui::Slider::new(&mut age, 0..=120).text("age")); 
      if self.button("Click each year").clicked() { 
         age += 1; 
      } 
      self.label(format!("Hello '{name}', age {age}"));
   };
};

This code looks better than in the example.

Better than macros

Possible you ask: "But we've procmacros! Why we cannot just use them instead?".

There are two reasons:

  1. Autocomplete. Unfortunately rust-analyzer cannot autocomplete you in procmacros as good as in regular code.
  2. Readability. Code in macros has worse readability than a regular code and in some macros we haven't a Rust-way syntax.
5 Likes

There is a syntax complication around if because of the lack of parens:

if true.then() { true }.unwrap_or_default() { return }

is this

if true.then(|| { true }).unwrap_or_default() {
  return
}

or

if true.then() {
  true
}
.unwrap_or_default() // syntax error: missing ;
{
  return
}
6 Likes

I think it's needed to disallow this syntax in: while, if, else if and for

Funnily enough, Rust (early, pre-1.0) once had this exact feature, and in fact it was how today’s for..in loop was done then. The idea was adopted from Ruby, which is plausibly what inspired Kotlin’s designers as well. (Scala 2 can also do this, but true to form it achieves it via two general, orthogonal features without needing special syntax: curryable functions and the fact that () and {} are interchangeable in function calls.)

3 Likes

Note that Kotlin's brace syntax is designed to synnergize with Kotlin's semantics of inline lambdas. This is discussed a bit in

https://without.boats/blog/the-problem-of-effects/

It seems reasonable that, if you make a lambda look like a code block, you should also make return, continue, await, ? and other control flow effects work the same way they work in code blocks.

https://effekt-lang.org is an interesting language exploring these ideas, with the core idea being:

if closures are second-class, they are naturally polymorphic over effects

7 Likes

(NOT A CONTRIBUTION)

It's interesting that you make the connection between my blog post and Effekt. I agree that there's a similarity.

The primary difference is that Effekt is a language with a first class effect system (as another kind in its type system other than types), whereas I was imagining a language without any effect system, but with some effectual aspects captured in the standard type system. Rust is a language like this. Some type signatures encode the same kind of state that's encoded in effects in a language with an effect system - e.g. Result, Future, Iterator.

A problem languages with higher order functions encounter is that whether they use an effect system or a type system to type these kinds of effectual states, the higher order function signature needs to somehow encode this in its signature. Indeed, it would perhaps be ideal if the function signature could instead be abstract over these kinds of things, in the same way it may be abstract over simple types that are not related to effects (e.g. just as map is agnostic to the type passed to it, it would ideally also be agnostic to the effects of the closure passed to it). But this is very challenging.

The trick my post is about, and the trick Effekt uses, is that you can avoid including these effects in the type signature of the closure if the closure cannot escape the surrounding context to be called later. Then these effectual states (encoded as types or as effects) can belong to the type of the calling scope around the call to the higher order function, instead of to the type of the closure. I called this "control-flow capturing closures," because it's very analogous to how closures capture variables from their surrounding scope.

In terms of adding this to Rust, the big problem (other than the inherent complexity of another kind of closure) is that it doesn't work well with external iterators, which do capture their closure for later, exactly the thing we can't allow!

2 Likes

For comparison, Swift has “trailing closures” with braces, but doesn’t propagate control flow. I do think it was worth it there, but it does mean you now have to pay attention to see if the braces are attached to a built-in control flow statement or not, and you can’t use the trailing syntax in conditionals, as pointed out before. (The tradeoff is slightly different because bare braces in Swift make a closure, not a block expression; inline blocks are always marked with do or another keyword.)

5 Likes

I think it's doable to require () in those cases; it wouldn't be the first ambiguity solved by adding parenthesis.

Thank you for idea! I'll add it to RFC as unresolved question

I think that in some cases, postfix type-dispatched macros could provide a poor man's version of control flow effect-ful closures. I mentioned this on Zulip: https://rust-lang.zulipchat.com/#narrow/stream/328082-t-lang.2Feffects/topic/Type-dispatched.20postfix.20macros.20as.20alternative.3F

1 Like

(w.r.t. the effects discussion, it's also interesting to consider the differences between composing and combining effects. Rust's type directed effects only really offers composition, but things like fallible generators[1] want more for combination instead.)

To OP: Kotlin's implicit receivers do a lot to enable eDSLs. I see you've acknowledged that for the Rust by as self (without further mention), but changing what self means is quite fraught. Not the least because now typechecking a closure using self needs to determine how it's used to know if it's a freshly bound receiver or the environmental captured self.

Currently type inference of closures only depends on the definition of the closure (and types of any captures), and usage (mostly; there are exceptions) doesn't flow backwards into the closure (e.g. any time rust-analyzer has successfully inferred the type of a closure argument but rustc refuses to). This would necessarily impact the behavior of inference here, and needs to lay out exactly how it is expected to.

I'm generally against (just passing the "receiver" explicitly is usually sufficient), but could potentially be convinced with a thorough enough proposal. But note also that Kotlin has the advantage of the tail closure syntax not changing when arguments are provided, which it necessarily would need to for Rust. So this limits the application to essentially only be useful for custom receiver (ab)using eDSLs.

The answer might instead be improving the experience of macro autocompletion. If your macro is error tolerant and always produces something roughly reasonable as output, r-a is already pretty good at providing autocompletion, surprisingly enough. (Most proc macros aren't, though, because it's much easier to only accept well-formed inputs, and that's syn's. I've been meaning to experiment with a lazily parsing version of syn....[2])


  1. as in iterators, not semicoroutines ↩︎

  2. I claim some corruption of sloth, the way syn can be a play on sin (i.e. no codebase is free from syn) and sloth is the deadly sin of laziness). You can't have it unless I'm on board. But I don't know what said corruption would be, though... :frowning: ↩︎

1 Like

Without throwing any opinion about the semantics, I think the problem with the syntax can easily be solved by borrowing from Rust's syntax instead of trying to imitate Kotlin's syntax.

In Kotlin, closures are written with the arguments inside the curly brackets ({ x -> x + 1 }) and if there are no arguments their entire notation can be omitted ({ 1 + 2 }). In Rust, the curly brackets are only needed if you have multiple statements, and if you use them the arguments are outside (|x| { x + 1 }). Even if you don't have arguments, you still need their notation (|| { 1 + 2 })

It follows, then, that instead of doing it like Kotlin with an optional argument list inside the curly brackets:

foo() { |x| x + 1 };
bar() { 1 + 2 }

We should do it like Rust, with a mandatory argument list outside them:

foo() |x| { x + 1 }
bar() || { 1 + 2 }

And of course, since it uses the regular closure syntax, the curly brackets can be omitted:

foo() |x| x + 1
bar() || 1 + 2

This resolves the problem, because in:

if true.then() || { true }.unwrap_or_default() { return }

The { true } part cannot be interpreted as the then clause of the if (because then the || will not have an expression after it) and the { return } part cannot be interpreted as the closure of unwrap_or_default() (because it does not have ||)

2 Likes

This is already valid syntax (playground):

struct True;
impl True {
    fn then(self) -> bool { true }
    fn unwrap_or_default(self) -> bool { true }
}
fn main() {
    if True.then() || { True }.unwrap_or_default() { return }
}

(with true it should just fail to find functions with the right signature).

Oh. Right. How could I forget the boolean or operator...

well, we could use a reserved keyword, maybe the do keyword, if no one wants it for anything else.

if option.unwrap_or_else() do || true { return }
2 Likes

I think all/most of the above mentioned problems can be solved by leaving the closure argument list inside the function call parens, like this:

foo.iter().map(|x|) {
    x * x + 1
}  

That way, I don't think ambiguities arise. The above if-example becomes:

if true.then(||) { true }.unwrap_or_default() { return }

As for control flow, it seems to me to be a non-issue. We already have the somewhat-unpleasant situation where we have to discern whether a return is inside a closure. For example, when you have closures assigned to a variable, like this:

lex callback = |x: T| {
    // things are being done
    return result;
    // ... more stuff ...
}

it's basically the same, I don't think the added closure block syntax would make that worse.

1 Like

At that point, what is the perceived benefit of

foo.iter().map(|x|) { x * x + 1 }

over

foo.iter().map(|x| { x * x + 1 })

since the difference is so miniscule? It's literally just the placement/nesting of a single closing parenthesis.

6 Likes

For me, the benfit is basically to avoid the trailing }) , which to me is a visual clutter and makes navigation in editor harder (when using parens/brace pair jump navigation). The single } is a lot easier for me to navigate both visually and with keyboard.

Yeah, it's a rather small change...

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.