Hacker News new | ask | show | jobs
by kennywinker 1739 days ago
Examples 3 and 4 are cursed, to my eye. Curly braces denote a block, so why is there some magic syntax inside the block that denotes that this block happens to be a lambda? surely the block is the closure itself. The syntax to say what the block is goes outside.

Imagine if i wrote a conditional like that:

    if x > y { return x; } else { return y; }
as

    { if x > y; return x; } { else; return y; }
It completely erases the usefulness of {}, and is cursed, cursed I say! And I’m looking at you, rust, swift, ruby, etc.
2 comments

I wholeheartedly agree. Whether {} denotes a block or a lambda is context-dependent in the language I took these examples from (Kotlin). I think they adopted the syntax from Groovy. It is "useful" for creating DSLs because you can implement constructs like `.forEach` so they look like imperative constructs, e.g.

    list.forEach { x -> ... }
But it becomes harder to read code outside an IDE, especially if the implicit `it` construct is used.
That's the thing that annoys me most about Kotlin (even though aside from that it is my personal 10X language). In a large project that uses DSL's, coroutines and flow it quickly becomes a curly brace mess and it gets hard to see what code runs where, when and in which context.
Same, I use Kotlin daily, and I would choose it over Java any day as it really solves a lot of its quirks very nicely. There are however some parts of the syntax that keep confusing me on a daily basis, curly braces being one of them. The other is the weird unintuitive asymmetry that is going on between function types and lambdas. For example, a lambda of type

    (Int, Float) -> Boolean
is written as

    { i, f -> ... body ... }
but a lambda of type

    (Pair<Int, Float>) -> Boolean
is written as

    { (i, f) -> ... body ... }
because `(x, y)` is a destructuring pattern that projects the first and second components into `x` and `y`, respectively. The pattern in the lambda has to use tuple syntax exactly when the type doesn't, and vice versa. Gets me on a weekly basis even though I completely understand what's going on, it's just not very ergonomic.
No, the idea is that {} denotes executable blocks, which lamdas are. In Kotlin, if the last parameter of a function is a lambda, you can close the parenthesis before the lambda:

  f(5, { square(it) }
can be written as

  f(5) { square(it) }
which makes constructs like

  if(condition) { foo() }
Look like

  if(condition, { foo() } )
as in a function that takes a boolean and a lambda and only executes the lambda if the boolean is true.

It's a neat reinterpretation of what {} means.

I’m not super familiar with kotlin, but swift does the same thing you’re describing so i get where it’s useful. My beef is with including metadata about the executable block (parameter names) inside the block. So per your example:

    f(5) { square(it) }
My preference would be to tag the fn signature on the outside of the braces, something like:

     f(5) [(x)->int]{ square(x) }
I’d also accept ||, (), or nothing as the delimiters around the fn signature. Key point is that it’s OUTSIDE the executable block.

I’m not opposed to using some smarts to infer/simplify the expression when possible. I.e. if it’s a closure with inferable parameter and return types the “it” construct could be used (in swift they use $0, $1, $2 etc for unnamed parameters). Just the only thing inside an executable block {} should be code that gets executed - not type information about that block