what you need to know about lambdas - jamie allen (typesafe)
DESCRIPTION
Presented as a keynote at JAX London 2013 Lambdas are coming to the Java language in the upcoming release of Java 8! While this is generally great news, many Java developers have never experienced Lambdas before, and have not yet learned the best ways to use them for maximum productivity. In this talk, we will discuss best practices for using Lambdas in Java and other JVM-based languages, and we will investigate how we can make these constructs more usable in production.TRANSCRIPT
Jamie Allen | Typesafe
What You Need to Know About Lambdas
Wednesday, November 6, 13
Who Am I?
• Director of Consulting for
• User of lambdas on the JVM for ~5 years
@jamie_allen
Wednesday, November 6, 13
Wednesday, November 6, 13
Wednesday, November 6, 13
Wednesday, November 6, 13
I Love Functional Programming!
• Functional Programming is:
• Immutability
• Referential Transparency
• Functions as first-class citizens
Wednesday, November 6, 13
We Want Declarative Codefinal List<Integer> numbers = Arrays.asList(1, 2, 3);
final List<Integer> numbersPlusOne = Collections.emptyList();
for (Integer number : numbers) { final Integer numberPlusOne = number + 1; numbersPlusOne.add(numberPlusOne);}
Wednesday, November 6, 13
What is a Lambda?
• A function literal
• Not bound to a variable name, can only be used in the context of where it is defined
• Merely one of many possible implementations you can use in Functional Programming
Wednesday, November 6, 13
Java 8import java.util.List;import java.util.Arrays;import java.util.stream.Collectors;
public class LambdaDemo { public static void main(String... args) { final List<Integer> numbers = Arrays.asList(1, 2, 3);
final List<Integer> numbersPlusOne = numbers.stream().map(number -> number + 1). collect(Collectors.toList()); }}
λ
Wednesday, November 6, 13
Nashorn Javascript#!/usr/bin/env jjs -scripting
var result = [];var list = new java.util.ArrayList();list.add(1);list.add(2);list.add(3);list.parallelStream(). map(function(e) e + 1). forEach(function(t) result.push(t));
λ
Wednesday, November 6, 13
Scala
object LambdaDemo extends App { val numbers = List(1, 2, 3) val numbersPlusOne = numbers.map(number => number + 1)}
λ
Wednesday, November 6, 13
Clojure
(ns LambdaDemo.core)(defn -main [& args] (println(map #(+ % 1) [1, 2, 3])))
λ
Wednesday, November 6, 13
JRuby
require "java"
array = [1, 2, 3]array.collect! do |n| n + 1end
λ
Wednesday, November 6, 13
What is the Problem?
Wednesday, November 6, 13
There Are Caveats
Wednesday, November 6, 13
Not Reusable
• Lambdas are limited in scope to their call site
• You cannot reuse the functionality elsewhere
Wednesday, November 6, 13
Not Testable in Isolation
• How can you test code by itself when you have no identifier through which you can call it?
• You can only test them by writing more tests for their enclosing method
Wednesday, November 6, 13
Maintainability• There is nothing inherently descriptive
about a lambda
• Developers have to read through the entire lambda to figure out what it is doing
• The more complex the lambda is, the harder this is to do
• Waste of valuable development time
Wednesday, November 6, 13
Example• In Scala, I sometimes see code like this:
val next = x.map { case Success(k) => { deriveValueAsynchronously(worker(initValue))(pec).map { case None => { val remainingWork = k(Input.EOF) success(remainingWork) None } case Some(read) => { val nextWork = k(Input.El(read)) Some(nextWork) } }(dec) } case _ => { success(it); Future.successful(None) } }(dec)
}
} }}}
λλλλλ
Wednesday, November 6, 13
Wednesday, November 6, 13
Lousy Stack Traces
• Compilers have to come up with generic names for their representation of lambdas to run on the JVM, called “name mangling”
• The stack trace output tells you very little about where the problem occurred
Wednesday, November 6, 13
Java 8
numbers.stream().map(number -> number / 0)
Exception in thread "main" java.lang.ArithmeticException: / by zero at LambdaDemo.lambda$0(LambdaDemo.java:9) at LambdaDemo$$Lambda$1.apply(Unknown Source) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:510) at LambdaDemo.main(LambdaDemo.java:9)
wat
Wednesday, November 6, 13
Nashorn Javascript
list.parallelStream(). map(function(e) e / 0)
[1, 2, 3]Infinity,Infinity
Wednesday, November 6, 13
Nashorn Javascript
list.parallelStream(). map(function(e) e / 0)
[1, 2, 3]Infinity,Infinity
Wednesday, November 6, 13
Favorite Tweet Ever
“JavaScript doesn't have a dark side, but it does have a dimly lit room full of angry clowns with rubber mallets.”
- @odetocode, Jan 5, 2010
Wednesday, November 6, 13
Scalaval numbersPlusOne = numbers.map(number => number / 0)
Exception in thread "main" java.lang.ArithmeticException: / by zero at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply(LambdaPlayground.scala:23) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply(LambdaPlayground.scala:23) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.immutable.Range.foreach(Range.scala:141) at scala.collection.TraversableLike$class.map(TraversableLike.scala:244) at scala.collection.AbstractTraversable.map(Traversable.scala:105) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$delayedInit$body.apply(LambdaPlayground.scala:23) at scala.Function0$class.apply$mcV$sp(Function0.scala:40) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32) at scala.App$class.main(App.scala:71) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$.main(LambdaPlayground.scala:22) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest.main(LambdaPlayground.scala)
wat
Wednesday, November 6, 13
Clojureprintln(map #(/ % 0) [1, 2, 3])))
Exception in thread "main" (java.lang.ArithmeticException: Divide by zero at clojure.lang.Numbers.divide(Numbers.java:156) at clojure.lang.Numbers.divide(Numbers.java:3671) at helloclj.core$_main$fn__10.invoke(core.clj:5) at clojure.core$map$fn__4087.invoke(core.clj:2432) at clojure.lang.LazySeq.sval(LazySeq.java:42) at clojure.lang.LazySeq.seq(LazySeq.java:60) at clojure.lang.RT.seq(RT.java:473) at clojure.core$seq.invoke(core.clj:133) at clojure.core$print_sequential.invoke(core_print.clj:46) at clojure.core$fn__5270.invoke(core_print.clj:140) at clojure.lang.MultiFn.invoke(MultiFn.java:167) at clojure.core$pr_on.invoke(core.clj:3266) at clojure.core$pr.invoke(core.clj:3278) at clojure.lang.AFn.applyToHelper(AFn.java:161) at clojure.lang.RestFn.applyTo(RestFn.java:132) at clojure.core$apply.invoke(core.clj:601) at clojure.core$prn.doInvoke(core.clj:3311) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.core$apply.invoke(core.clj:601) at clojure.core$println.doInvoke(core.clj:3331) at clojure.lang.RestFn.invoke(RestFn.java:408) at helloclj.core$_main.invoke(core.clj:5) at clojure.lang.Var.invoke(Var.java:411) ... at clojure.main.main(main.java:37)
wat
Wednesday, November 6, 13
JRubyarray.collect! do |n| n / 0
ZeroDivisionError: divided by 0 / at org/jruby/RubyFixnum.java:547 (root) at HelloWorld.rb:11 collect! at org/jruby/RubyArray.java:2385 (root) at HelloWorld.rb:10
not half bad, really
Wednesday, November 6, 13
Difficult Debugging• Debuggers on the JVM can only
disambiguate code at the source line - write your lambdas to leverage this
final List<Integer> numbersPlusOne = numbers.stream(). map(number -> number + 1).collect(Collectors.toList());
NO!
Wednesday, November 6, 13
Digression: Lambdas versus Closures
• In the purest sense, closures are merely lambdas that close over some state from outside of their enclosing scope
final int x = 1;final List<Integer> numbersPlusOne = numbers.stream().map(number -> number + x). collect(Collectors.toList());
Wednesday, November 6, 13
Closing Over State• Lambdas have access to all variables that are in
scope
• It is very easy to “close over” something mutable and cause headaches in multi-threaded code
• Java enforces that values to be closed over are final, but that only affects assignment - you can still change what is INSIDE that variable (like the contents of a collection)
Wednesday, November 6, 13
Solution
We want to maintain our ability to program in a functional style, while having something
maintainable and understandable in production
Wednesday, November 6, 13
Named Functions?
• Seems like it would help, but it depends on the compiler and how it manages the “scope” of that function
• It is possible that stack traces will still not show the name of the function
Wednesday, November 6, 13
Named Function
object LambdaTest extends App { val addOneToValue = (x: Int) => x + 1
val myList = (1 to 20).map(addOneToValue)}
Wednesday, November 6, 13
Named Function Stack Traceval badFunction = (x: Int) => x / 0val myList = (1 to 20).map(badFunction)
Exception in thread "main" java.lang.ArithmeticException: / by zero at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply$mcII$sp(LambdaPlayground.scala:23) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$2.apply(LambdaPlayground.scala:24) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$2.apply(LambdaPlayground.scala:24) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.immutable.Range.foreach(Range.scala:141) at scala.collection.TraversableLike$class.map(TraversableLike.scala:244) at scala.collection.AbstractTraversable.map(Traversable.scala:105) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$delayedInit$body.apply(LambdaPlayground.scala:24) at scala.Function0$class.apply$mcV$sp(Function0.scala:40) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32) at scala.App$class.main(App.scala:71) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$.main(LambdaPlayground.scala:22) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest.main(LambdaPlayground.scala) wat
Wednesday, November 6, 13
“Lifting” a Method
• We have the ability in Java 8 and Scala to “lift” or coerce a method into a function
• The method must meet the contract of the lambda usage of the compiler, such as only taking one argument representing the input of the function
Wednesday, November 6, 13
Stack Trace of a Methoddef badFunction = (x: Int) => x / 0val myList = (1 to 20).map(badFunction)
Exception in thread "main" java.lang.ArithmeticException: / by zero at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$badFunction$1.apply$mcII$sp(LambdaPlayground.scala:23) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply(LambdaPlayground.scala:24) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply(LambdaPlayground.scala:24) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.immutable.Range.foreach(Range.scala:141) at scala.collection.TraversableLike$class.map(TraversableLike.scala:244) at scala.collection.AbstractTraversable.map(Traversable.scala:105) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$delayedInit$body.apply(LambdaPlayground.scala:24) at scala.Function0$class.apply$mcV$sp(Function0.scala:40) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32) at scala.App$class.main(App.scala:71) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$.main(LambdaPlayground.scala:22) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest.main(LambdaPlayground.scala)
Better, but why $1
Wednesday, November 6, 13
Digression• You can define your methods like that, but
“def” is not stable - it will reevaluate the right side of the equals sign and return a new but identical function each time!
def badFunction = (x: Int) => x / 0
def badFunction(x: Int) = x / 0
• Better to stick with simple method syntax instead
Wednesday, November 6, 13
Stack Trace of a Stable Methoddef badFunction(x: Int) = x / 0val myList = (1 to 20).map(badFunction)
Exception in thread "main" java.lang.ArithmeticException: / by zero at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$.badFunction(LambdaPlayground.scala:24) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply(LambdaPlayground.scala:25) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$$anonfun$1.apply(LambdaPlayground.scala:25) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.immutable.Range.foreach(Range.scala:141) at scala.collection.TraversableLike$class.map(TraversableLike.scala:244) at scala.collection.AbstractTraversable.map(Traversable.scala:105) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$delayedInit$body.apply(LambdaPlayground.scala:25) at scala.Function0$class.apply$mcV$sp(Function0.scala:40) at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.App$$anonfun$main$1.apply(App.scala:71) at scala.collection.immutable.List.foreach(List.scala:318) at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32) at scala.App$class.main(App.scala:71) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest$.main(LambdaPlayground.scala:22) at org.jamieallen.effectiveakka.pattern.extra.LambdaTest.main(LambdaPlayground.scala) Perfect!
Wednesday, November 6, 13
Benefits• You can’t close over variables
• Better stack traces
• More debuggable
• More testable
• More maintainable and descriptive
• Reusable
Wednesday, November 6, 13
Rule of Thumb
• Reserve lambda usage for the most basic expressions
• Externalize anything more significant than that to methods
Wednesday, November 6, 13
Language Creators
• Language designers and tool producers need to help us
• At Typesafe, we’re making our Eclipse-based Scala IDE more lambda-friendly with each release
Wednesday, November 6, 13
Thank You!
• Questions?
Wednesday, November 6, 13