Multi-declarations in Parameters

  • Timeframe: M10
  • Why: Breaking changes in syntactic structures (lambdas)

Goal

Support multi-declarations in parameters of lambdas, functions, constructors and setters.

Examples

// decomposing pairs in a lambda
listOfPairs.map {
  (a, b) -> a + b
}

// decompose a parameter:
fun foo((a, b): Pair<Int, String>, c: Bar)
// can be called as
foo(pair, bar)

// decompose a constructor parameter
class C(val (a, b): Pair<Int, String>) {}

TODO

  • names of decomposed parameters in Kotlin, e.g. for named arguments

Syntax

Old lambda syntax:

{ a, b -> ... } // two parameters
{ (a, b) -> ... } // two parameters
{ (a: Int, b: String) -> ... } // parameter types
{ (a, b): Int -> ... } // return type
{ T.(a, b): Int -> ... } // receiver type

New syntax:

Common short form:

{ a -> ... } // one parameter
{ a, b -> ... } // two parameters
{ (a, b) -> ... } // a decomposed pair
{ (a, b), c -> ... } // a decomposed pair and another parameter
{ ((a, b), c) -> ... } // ??? a decomposed pair whose first component is a pair

No return type nor receiver type in the short form:

{ a: A -> ... } // one parameter
{ a, b: B -> ... } // two parameters
{ (a, b: B) -> ... } // a decomposed pair
{ (a, b): Pair<A, B> -> ... } // a decomposed pair
{ (a, b: B), c -> ... } // a decomposed pair and another parameter
{ ((a, b), c: C) -> ... } // ??? a decomposed pair whose first component is a pair

(BAD OPTION) To disambiguate, we could demand a prefix:

{ fun Recv.(((a: A, b: B): Pair<A, B>, c: C): Pair<Pair<A, B>, C>): R -> ... } // ??? a decomposed pair whose first component is a pair
{ fun (((a: A, b: B): Pair<A, B>, c: C): Pair<Pair<A, B>, C>): R -> ... } // ??? a decomposed pair whose first component is a pair
{ fun ( ((a, b), c) ): R -> ... } // ??? a decomposed pair whose first component is a pair
{ fun (((a, b), c: C)) -> ... } // ??? a decomposed pair whose first component is a pair
{ fun (((a, b), c): Pair<Pair<A, B>, C>) -> ... } // ??? a decomposed pair whose first component is a pair

Rather hairy.

But we have this form coming (needed for local returns):

foo(fun(): R {
    return r // local return
})

We can't omit return type in this form. But we use it only when we need return type/receiver type:

fun Recv.(((a: A, b: B): Pair<A, B>, c: C): Pair<Pair<A, B>, C>): R { ... } // a decomposed pair whose first component is a pair
fun (((a: A, b: B): Pair<A, B>, c: C): Pair<Pair<A, B>, C>): R { ... } // a decomposed pair whose first component is a pair
fun ( ((a, b), c) ): R { ... } // ??? a decomposed pair whose first component is a pair
fun (((a, b), c: C)): R { ... } // ??? a decomposed pair whose first component is a pair
fun (((a, b), c): Pair<Pair<A, B>, C>): R { ... } // ??? a decomposed pair whose first component is a pair
fun (a) {} // return type is Unit

Difference from normal functions: we can omit parameter types, we can omit the name (don't have to). Difference from lambdas: can specify return type and receiver type + returns are local.

Quick summary of syntactic changes

  • functions
    • allow multi-declarations in parameters
    • allow functions as expressions (anonymous or named)
  • lambdas
    • allow multi-declarations in lambda parameters
    • no return type/receiver type in lambda parameters, use anonymous function instead

Grammar

TODO

PSI changes

Create a common superclass for lambdas and anonymous functions, most clients shouldn't notice the change

Front-end checks and language rules

New concept introduced: “function expression” (looks like a function declaration, but works as an expression) as opposed to “lambda” (both are special cases of “function literal”).

Function call sites

From the outside a multi-declared parameter is seen as one parameter of the specified type:

foo(pair) // caller can not pass two separate values here

No changes to the call-site checking are required.

Function declaration sites

Function declarations are not allowed to omit types of their parameters:

fun foo((a, b): Pair<A, B>) {...} // type is required

Types of individual components of the multi-declarations are optional:

fun foo((a: A, b: B): Pair<A, B>) {...} // individual types of `a` and `b` are not required

Default values are only allowed for whole parameters, not for individual components:

fun foo((a, b): AB = AB(1, 2)) {...}

All names in the parameter list belong to one and the same namespace:

fun foo((a, b): AB, a: A) // redeclaration: two variables named `a`

One can use components of previously declared parameters in default values:

fun foo((a, b): AB, c: C = C(a, b)) {...}

A parameter can be decomposed iff there are appropriate component functions available at the declaration site:

fun Int.component1() = 1
fun Int.component2() = 1
fun foo((a, b): Int) {...}

other wise it's an error.

Component functions must be checked against the declared types of component parameters if they are present:

fun foo((a: String, b): Int) {...} // error: Int.component1()'s return type is Int, incompatible with String

Function expressions

Function expression syntax differs from function declaration syntax in the following ways:

  • function name can be omitted
    • consequently, receiver type can precede the parameter list directly
  • type parameters are not allowed
  • where clause is not allowed
  • parameter types can be omitted (even for decomposed parameters)
  • parameter default values are not allowed
  • varargs are allowed, but useless (warning issued)

NOTE: local returns are allowed in function expressions without qualification. ISSUE: when a function expression is inlined, unqualified returns must remain local. Wouldn't this confuse the reader?

NOTE: function expression can not be passed to a function call outside the parentheses

Lambda expressions

In a lambda, only parameters (possibly decomposed) and their types can be specified. There's no way to explicitly specify the return type or receiver type. Those have to be inferred, otherwise function expression must be used.

TODO: support qualified returns in lambdas (when return type is unknown, nad has to be inferred).

Nested multi-declarations

Example:

val (a, (b, c)) = abc // e.g. of type Pair<A, Pair<B, C>>

This translates to

tmp1 <- abc
a <- tmp1.component1()
tmp2 <- tmp1.component2()
b <- tmp2.component1()
c <- tmp2.component2()

If some of the types of a, b or c are specified, then front-end verifies that respective component function results match the expected types.

Biggest issue: type inference for function literals.

Expected type known entirely:

fun foo((Pair<A, Pair<B, C>>) -> Unit) {}

foo { (a, (b, c)) -> ... }

In this case all we need is check that appropriate component functions are available (and that their types match specifications, if any).

Expected type contains type parameters:

fun <T> foo(t: T, (T) -> Unit) {...}

foo(ABC) {(a, (b, c)) -> ...}

In this case we can't check the component conventions before T is fully resolved to a type.

It seems that this does not impose any significant issues on the inference, and can go right before the normal type checking of the body of a lambda.

Semantics and Back-end changes

TODO

  • what is the Java name of this parameter: (a, b): Pair<A, B>?
    • maybe a_b
  • make components available in default parameter values
  • create locals for components, assign values (on nested decompositions, avoid calling the same component twice)
  • make sure that function expressions are inlined as well as lambda expressions

IDE Changes

New intentions:

  • Convert lambda <-> anonymous function (mind the returns!)

Affected functionality:

  • Change signature
  • Move lambda outside/inside parentheses
  • Specify types explicitly in a lambda (use conversion to anonymous function)