10
votes

As I understand, in languages such as Haskell, and also as part of the lambda calculus, each lambda expression has its own scope, so if I have nested lambda expressions such as: \x -> (\x -> x) then the first \x parameter is different to the second \x.

In Java if you do this you get a compilation error, just like if you use x again as the parameter name or a local variable name within the lambda if it has already been used inside the enclosing scope, e.g. as a method parameter.

Does anybody know why Java implemented lambda expressions this way - why not have them introduce a new level of scope and behave like an anonymous class would? I'm assuming it's because of some limitation or optimisation, or possibly because lambdas had to be hacked into the existing language?

4
How do you refer to the outer x within the nested lambda in such languages?Sotirios Delimanolis
@SotiriosDelimanolis You don't necessarily have to be able to, this is a design decision. You can e. g. also not access a local variable x from an anonymous class where you define another x which is perfectly valid.Vampire

4 Answers

12
votes

This is the same behaviour as for other code blocks in Java.

This gives a compilation error

int a;
{
    int a;
}

while this does not

{
    int a;
}
{
    int a;
}

You can read about this topic in section 6.4 of the JLS, together with some reasoning.

4
votes

A lambda block is a new block, aka scope, but it does not establish a new context/level, like an anonymous class implementation does.

From Java Language Specification 15.27.2 Lambda Body:

Unlike code appearing in anonymous class declarations, the meaning of names and the this and super keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).

And from JLS 6.4 Shadowing and Obscuring:

These rules allow redeclaration of a variable or local class in nested class declarations (local classes (§14.3) and anonymous classes (§15.9)) that occur in the scope of the variable or local class. Thus, the declaration of a formal parameter, local variable, or local class may be shadowed in a class declaration nested within a method, constructor, or lambda expression; and the declaration of an exception parameter may be shadowed inside a class declaration nested within the Block of the catch clause.

There are two design alternatives for handling name clashes created by lambda parameters and other variables declared in lambda expressions. One is to mimic class declarations: like local classes, lambda expressions introduce a new "level" for names, and all variable names outside the expression can be redeclared. Another is a "local" strategy: like catch clauses, for loops, and blocks, lambda expressions operate at the same "level" as the enclosing context, and local variables outside the expression cannot be shadowed. The above rules use the local strategy; there is no special dispensation that allows a variable declared in a lambda expression to shadow a variable declared in an enclosing method.

Example:

class Test {
    private int f;
    public void test() {
        int a;
        a = this.f;     // VALID
        {
            int a;      // ERROR: Duplicate local variable a
            a = this.f; // VALID
        }
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                int a;           // VALID (new context)
                a = this.f;      // ERROR: f cannot be resolved or is not a field
                                 //   (this refers to the instance of Runnable)
                a = Test.this.f; // VALID
            }
        };
        Runnable r2 = () -> {
            int a;      // ERROR: Lambda expression's local variable a cannot redeclare another local variable defined in an enclosing scope.
            a = this.f; // VALID
        };
    }
}
2
votes

Lambdas in Java do introduce a new scope - any variable declared in a lambda is only accessible within the lambda.

What you really ask about is shadowing - changing binding of a variable already bound in some outer scope.

It is logical to allow some level of shadowing: you want to be able to shadow global names by local names, because otherwise you can break local code just by adding a new name to some global namespace. A lot of langues, for sake of simplicity, simply extend this rule down to local names.

On the other hand, rebinding local names is a code smell and can be a source of subtle mistakes, while - at the same time - not offering any technical advantage. Since you mentioned Haskell, you can look at this discussion on Lambda the Ultimate.

This is why Java disallows shadowing of local variables (like many other potentially dangerous things), but allows shadowing attributes by local variables (so that adding attributes will never break a method that already used the name).

So, the designers of Java 8 had to answer a question if lambdas should behave more like code blocks (no shadowing) or like inner classes (shadowing) and made a conscious decision to treat them like the former.

0
votes

While the other answers make it seem like this was a clear-cut decision by the language designers, there is actually a JEP that proposes to introduce shadowing for lambda parameters (emphasis mine):

Lambda parameters are not allowed to shadow variables in the enclosing scopes. [...] It would be desirable to lift this restriction, and allow lambda parameters (and locals declared with a lambda) to shadow variables defined in enclosing scopes.

The proposal is relatively old and has obviously not found its way into the JDK yet. But since it also includes a better treatment of the underscore (which was deprecated as an identifier in Java 8 to pave the way for this treatment), I could image that the proposal as a whole is not completely off the table.