Skip to content

Lambdas

If you want to write reusable blocks of code, use a segment. However, sometimes you need to create a highly application-specific callable that can be passed as argument to some function or returned as the result of a segment. We will explain this concept by filtering a list. Here are the relevant declarations:

class IntList {
    fun filter(filterFunction: (element: Int) -> shouldKeep: Boolean) -> filteredList: IntList
}

fun intListOf(elements: List<Int>) -> result: IntList

First, we declare a class IntList, which has a single method called filter. The filter method returns a single result called filteredList, which is a new IntList. filteredList is supposed to only contain the elements of the receiving IntList for which the filterFunction parameter returns true.

Second, we declare a global function intListOf that is supposed to wrap elements into an IntList.

Say, we now want to keep only the elements in the list that are less than 10. We can do this by declaring a segment:

segment keepLessThan10(a: Int) -> shouldKeep: Boolean {
    yield shouldKeep = a < 10;
}

Here is how to solve the task of keeping only elements below 10 with this segment:

intListOf(1, 4, 11).filter(keepLessThan10)

The call to intListOf is just there to create an IntList that we can use for filtering. The interesting part is the argument we pass to the filter method, which is simply a reference to the segment we declared above.

The problem here is that this solution is very cumbersome and verbose. We need to come up with a name for a segment that we will likely use only once. Moreover, the segment must declare the types of its parameters and its results in its header. Finally, the declaration of the segment has to happen in a separate location then its use. We can solve those issues with lambdas.

Block Lambdas

We will first rewrite the above solution using a block lambda, which is essentially a segment without a name and more concise syntax that can be declared where it is needed:

intListOf(1, 4, 11).filter(
    (a) { yield shouldKeep = a < 10; }
)

While this appears longer than the solution with segments, note that it replaces both the declaration of the segment as well as the reference to it.

Here are the syntactic elements:

  • A list of parameters, which is enclosed in parentheses. Individual parameters are separated by commas.
  • The body, which is a list of statements enclosed in curly braces. Note that each statement is terminated by a semicolon.

The results of a block lambda are declared in its body using assignments.

Declare Results of Block Lambdas

Similar syntax is used to yield results of block lambdas. The difference to segments is that block lambdas do not declare their results in their header. Instead the results are declared within the assignments, just like placeholders. The block lambda in the following snippet has a single result called greeting, which gets the value "Hello, world!":

() -> {
    yield greeting = "Hello, world!";
}

The assignment here has the following syntactic elements:

  • The keyword yield, which indicates that we want to declare a result.
  • The name of the result, here result. This can be any combination of upper- and lowercase letters, underscores, and numbers, as long as it does not start with a number. However, we suggest using lowerCamelCase for the names of results.
  • An = sign.
  • The expression to evaluate (right-hand side).
  • A semicolon at the end.

Expression Lambdas

Often, the body of a block lambda only consists of yielding a single result, as is the case in the example above. The syntax of block lambdas is quite verbose for such a common use-case, which is why Safe-DS has expression lambdas as a shorter but less flexible alternative. Using an expression lambda we can rewrite the example above as

intListOf(1, 4, 11).filter(
    (a) -> a < 10
)

These are the syntactic elements:

  • A list of parameters, which is enclosed in parentheses. Individual parameters are separated by commas.
  • An arrow ->.
  • The expression that should be returned.

Closures

Note: This is advanced concept, so feel free to skip this section initially.

Both block lambdas and expression lambdas are closures, which means they remember the values of placeholders and parameters that can be accessed within their body at the time of their creation. Here is an example:

segment lazyValue(value: Int) -> result: () -> storedValue: Int {
    yield result = () -> value
}

This deserves further explanation: We declare a segment lazyValue. It takes a single required parameter value with type Int. It produces a single result called result, which has a callable type that takes no parameters and produces a single result called storedValue with type Int. In the body of the segment we then assign an expression lambda to the result result.

The interesting part here is that we refer to to the parameter value within the expression of the lambda. Since lambdas are closures, this means the current value is stored when the lambda is created. When we later call this lambda, exactly this value is returned.

Restrictions

At the moment, lambdas can only be used if the context determines the type of its parameters. Concretely, this means we can use lambdas in these two places: