6
$\begingroup$

How can multiline lambdas be designed in indent based languages?

In Go, language with curly brackets, I will write multiline lambdas like this:

. . .
func () {
    ...;
    ...;
    ...;
}

But how can multiline lambdas be written in indent based language?

$\endgroup$
2
  • $\begingroup$ Is your question focused on implementing the lambdas or merely the design of them? $\endgroup$ Commented Jun 5, 2023 at 9:53
  • $\begingroup$ @FireTheLost designing. $\endgroup$ Commented Jun 5, 2023 at 9:54

4 Answers 4

9
$\begingroup$

In "indent-based languages" such as Python, indentation is needed at the statement level to determine the nesting of blocks. On the other hand, the nesting of expressions is defined without reference to indentation or any other whitespace, so there is no need to enforce any rules about indentation at the expression level, and arbitrary indentation is allowed when splitting expressions across multiple lines, either using backslash \ to ignore a line ending or writing inside brackets.

Both of these can be implemented directly in the lexer:

  • Recognise \\\n\s* (i.e. a literal backslash followed by a newline followed by any whitespace) as a whitespace token. It will then be ignored when parsing the expression, just like any other whitespace, and it won't get processed by the algorithm which tracks indentation at newlines because the newline character is part of the token.
  • The lexer counts the nesting level of brackets (increment a counter on each (, [ or { token, decrement on ), ] or }), and the algorithm which tracks indentation and inserts INDENT and DEDENT tokens ignores newlines when the counter is greater than zero. (A simple counter is enough, because if they don't match up correctly the parser will notice and report the error.)

This is all enough for lambda functions to span multiple lines when the body of the lambda is an expression ─ the user just has to ensure that either the lambda or its body is wrapped in parentheses, then it can span multiple lines. (Of course, this applies to every expression, not just lambdas.)


Lambda functions whose body is a block of statements, on the other hand, are considerably more difficult. Python's solution is to just not allow such lambdas; instead, you are supposed to declare a named function and then you can refer to it by name in an expression. There's a lot to be said for this approach ─ it means that expressions never contain statements, making the language grammatically simpler. It also results in more useful stack traces on runtime errors, because the lines have meaningful names instead of just saying <lambda>.

On the other hand, if you do want statements inside expressions, there's probably no way around having some kind of delimiters (like the braces in Rust), otherwise there will be nothing but whitespace separating the end of the last statement from the continuation of the expression that the lambda occurred in. Here's what it looks like without delimiters:

foo(lambda:
    print('hello world')
    do_something_else()
, 3.5)

Without block delimiters, the comma is required to be in this ugly position at the start of the line, and moreover the indentation of the expression matters on that line ─ it must be less indented than the lambda body ─ when indentation inside expressions doesn't matter anywhere else. These problems both disappear when you require delimiters; if you don't like braces in an indentation-based language, you could use Lua-style do/end keywords instead:

foo(lambda: do
    print('hello world')
    do_something_else()
end, 3.5)

So I think you have to just bite the bullet and use block delimiters, at least for statements that occur inside expressions. You don't need to require delimiters on every block, and you can still enforce the language's indentation rules between those delimiters; they're not there to define the nesting level of statements, they're there to separate the statements from the expression which contains them.

Implementation-wise, the lexer now needs to maintain a stack of those counters, so that when you see a do token it pushes a 0 to the stack, and when you see end it pops the counter from the stack. (No need to check that it's zero; the parser will ensure correct nesting of brackets.) This way you effectively "reset" the counter so that the lexer can insert the correct INDENT and DEDENT tokens inside the lambda body, while still being able to restore the old counter for the nesting level of the expression that the lambda occurs in.

$\endgroup$
1
  • $\begingroup$ There are other indentation-sensitive languages that do care about indentation of nested sub-expressions. The key issue with Python is that it disregards all indentation that's inside parenthesis or brackets. $\endgroup$
    – hugomg
    Commented Feb 21 at 23:04
4
$\begingroup$

Koka's trailing lambda syntax

The Koka language has an interesting feature called trailing lambdas: when the last argument of a function is a lambda, it can be written outside of the parentheses.

Let's take a look at this example (based on the example in kaya3's answer):

foo(3.5, fn()
    println('hello world')
    do_something_else()
)

The final right-parenthesis is ugly. Koka allows us to write this instead:

foo(3.5) fn()
    println('hello world')
    do_something_else()

Since this lambda doesn't take any arguments, the fn() can be omitted:

foo(3.5)
    println('hello world')
    do_something_else()

I think this is an elegant syntax sugar. There are some other languages that have similar feature, such as Julia and some early version of Rust, but those languages are not indent based.

$\endgroup$
1
  • 1
    $\begingroup$ Swift and Kotlin also have trailing closure syntaxes that look like foo(3.5) { print("hello world"); doSomethingElse() }. You can even omit the explicit parameter if there is one, as it gets an automatic default name ($0 in Swift and it in Kotlin). $\endgroup$
    – Bbrk24
    Commented Jul 5, 2023 at 14:54
3
$\begingroup$

Like in F#:

List.map (fun x ->
    printfn "Got %d" x;
    x*2) [1..10]

or

List.map
    (fun x ->
        printfn "Got %d" x;
        x*2)
    [1..10]

Again, let me repeat @kaya3 and point out that this is only relevant to several statements inside a lambda-function. Expressions are not sensitive to whitespace, newlines and indentation in most programming languages, so one can format a single expression in any way they like.

As for the statements... In an expression-based language you're supposed to be able to wrap anything in parenthesis, which basically solves the problem "automatically".

$\endgroup$
1
$\begingroup$

In my language Déjà Vu, anonymous functions were written like this:

labda x:
    do-something-with x
    another-line

They need to take up the entire line. It being a stack-based language, using a lambda (spelled labda in Déjà Vu because of reasons) as an argument requires you to have the function on the next line after that:

# Python: takes_callback(lambda: ...)
# Déjà Vu:

labda:
    ...
takes-callback

Returning them can be done implicitly:

# Python:
# def counter():
#     i = 0
#     def _anonymous():
#         nonlocal i
#         current = i
#         i += 1
#         return i
#     return _anonymous

# Déjà Vu:
func counter:
    local :i 0
    labda:
        i
        set :i + 1 i
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .