Thursday, July 15, 2010

Lambdas in Java Preview - Part 4: Proposal Update

This is the fourth part in a series of blog posts (read part 3 and part 5) giving some practical examples of lambdas, how functional programming in Java could look like and how lambdas could affect some of the well known libraries in Java land. This part describes shortly the changes of a new proposal, that has been published while writing this series, and how it changes some of the examples in this series.

The new proposal
Last week Brian Goetz published a new proposal to add lambda expressions to Java, that builds on the previous straw-man proposal. The two biggest changes are that function types have been removed and that there will be more type inferencing than before. Also the new proposal uses a different syntax for lambda expressions, but it also states, that this doesn't reflect a final decision on the syntax. But let's have a short look at each of these points.

No more function types
At first, this sounds irritating. Without function types, how could we pass functions to other methods or functions? We need to declare them as parameters somehow. The answer is that functions or better lambda expressions have to be converted to SAM types. Remember that SAM types are single abstract method types, like interfaces with only one method or abstract classes with only one abstract method, and that lambda expressions can automatically be converted to such types. This is called SAM conversion, which I've also discussed in the first part of this series.

So, function types will be primarily replaced by SAM conversion, lambda expressions can only appear in places where they can be converted to a SAM type. I don't fully understand the reasons behind this decision, but in a way it actually makes sense. As shortly mentioned in the first part of this series, from an API designer's perspective you don't have to scratch your head anymore if your methods should take a function type or a SAM type as an argument. And SAM types would probably turn out to be more flexible, anyway.

However, this will change almost all of the examples in this series. In all places, where a method took a function as an argument, the type of this argument must be changed to a SAM type, and I've almost everywhere used function types. We'll have a look at this later, and see if it makes sense now to have some generic function interfaces for functions like Function<X,Y>.

More type inferencing
As we've also seen in the examples in this series, lambda expressions can be quite verbose, because of type declarations. The new proposal says, that basically all types in lambda expressions can be inferred by the compiler - their return types, the parameter types and the exception types. This will make lambdas much more readable.

Alternative syntax
As said, the syntax of the new proposal does not reflect a final decision, but it is much more like in Scala or Groovy in this proposal.
FileFilter ff = {file -> 
    file.getName().endsWith(".java") };
Although I personally like this arrow syntax much better, I'll stick with the #-syntax in this series because the prototype uses it.

More changes
Just for completeness, the proposal contains some more changes. this in lambdas has different semantics than before, break and continue aren't allowed in lambdas, and instead of return, yield will be used to return values from a lambda expression (there are no details on how yield exactly works). Also, there will be method references, that allow for referencing methods of an existing class or object instance.

Impact on the examples
Now, let's have a look at how the new proposal changes some examples of the previous posts. The file filtering example from part 1 looked like this:
public static File[] fileFilter(File dir, #boolean(File) matcher) {
    List<File> files = new ArrayList<File>();
    for (File f : dir.listFiles()) {
        if (matcher.(f)) files.add(f);
    }
    return files.toArray(new File[files.size()]);
}
The client code looked like this:
File[] files = fileFilter(dir, #(File f)(f.getName().endsWith(".java")));
As there will be no function types anymore, we will have to change the fileFilter, so that it takes a SAM type instead of a function type. In this case it's simple, we could simply change the second argument's type to FileFilter and call its accept() method:
public static File[] fileFilter(File dir, FileFilter matcher) {
    List<File> files = new ArrayList<File>();
    for (File f : dir.listFiles()) {
        if (matcher.accept(f)) files.add(f);
    }
    return files.toArray(new File[files.size()]);
}
The client side is still the same, though.

Now, as you might have noticed (or remembered from part 2) this is quite a bad example, because there actually is already the File.listFiles(FileFilter) method we can also call with a lambda:
File[] files = dir.listFiles(#(File f)(f.getName().endsWith(".java")));
The lambda will be converted to a FileFilter automatically in this case. But despite the fileFilter method is a bad example, it shows quite well that the removal of function types has very little impact, if there is already a corresponding SAM type.

Before we go further, here's the client side with the arrow syntax and type inference:
File[] files = fileFilter(dir, f -> { 
    f.getName().endsWith(".java") });
Now, let's see how the removal of function types changes the examples in the abscence of an appropriate SAM type. For that here is the List<T>.map function from part 3 again:
public <S> List<S> map(#S(T) f) {
    List<S> result = new ArrayList<S>();
    for (T e : this) {
        result.add(f.(e));        
    }    
    return result;
}
We need to replace its function type argument f by a SAM type. We could start by writing an interface Mapper with a single method map. This would be an interface especially for usage with the map function. But having in mind, that there are probably many more functions similar to our map function, we could create a more generic Function interface, or more specifically an interface Function1<X1,Y> which represents a function that takes exactly one argument of type X1 and returns a value of type Y.
public interface Function1<X1,Y> {
    Y apply(X1 x1);
}
With this we could change our map function taking a Function1 instead of a function type argument:
public <S> List<S> map(Function1<T,S> f) {
    List<S> result = new ArrayList<S>();
    for (T e : this) {
        result.add(f.apply(e));        
    }    
    return result;
}
Again, the client side would still be the same. The lambda expression will be converted to a Function1.
List<Integer> list = new List<Integer>(2,4,2,5);
List<String> result = list.map1(n -> {"Hello World".substring(n)});
The other examples are basically the same, so I think this is enough to show the changes that the new proposal brings by removing function types.

Finally let's have a short look at the new syntax and the stronger type inferencing. With the initial proposal a File.eachLine method, could be called like this (see also part 2).
file.eachLine(#(String line) {
  System.out.println(line);
});
This would look more like this with the new proposal's syntax:
file.eachLine(line -> {
  System.out.println(line);
});
And with some more syntactic sugar, e.g. if it was allowed to remove parentheses, leave out a lambdas argument if it just takes a single argument and call it it in this case, then it would look even more like a groovy control structure:
file.eachLine {
  System.out.println(it);
};
But this is not part of the any proposal, yet.


To summarize, the removal of function types does not have such a huge impact on the examples, and in a way it removes complexity and feels more Java-like. To me it would be quite reasonable to have generic function SAM types in the standard libraries, e.g. Function0 for a function that takes no arguments, Function1 for a function that takes one argument and so on (up to Function23, then Java would have more than Scala, yay!). Further complexity is removed by the stronger type inferencing capabilities.



4 comments:

Adam Rabung said...

Great summary! Do you know if they will be adding things like map/filter to Collections API in Java 7? If so, will it be in the "immutable" style of your example, or will map() alter the collection?

Nick Wiedenbrueck said...

I strongly believe, that such methods will be added to the collections. That's the primary reason why they are thinking about extension methods. If they actually would be added, I'd guess they will return new collection instances instead of modifying the existing instance.

blakez said...

yes the new syntax is clearer

I wonder what the impact will be on IDE's? Is this going to be hard to implement?, will it slow IDE UI down ?

Neale said...

Much cleaner, and I think that dropping the function types is a good idea, as it will make it easier on IDE developers and on those who instrument and weave byte code (e.g. BCEL, ASM, AspectJ).