This document summarizes Jamie Allen's presentation on lambdas. Some key points:
- Lambdas are anonymous functions that are limited in scope and difficult to test and debug in isolation.
- Using named functions instead of lambdas can help with readability and debugging by showing the function name in stack traces, but the name may still be mangled.
- Lambdas have access to variables in enclosing scopes, which can cause problems if mutable state is closed over.
- To maintain functional programming benefits while improving maintainability, techniques like "lifting" methods to treat them as first-class functions can help. This allows showing the method name in stack traces.
6. I Love Functional Programming!
• Functional Programming is:
• Immutability
• Referential Transparency
• Functions as first-class citizens
Wednesday, November 6, 13
7. We Want Declarative Code
final 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
8. 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
9. Java 8
import 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);
}
}
Wednesday, November 6, 13
λ
final List<Integer> numbersPlusOne =
numbers.stream().map(number -> number + 1).
collect(Collectors.toList());
10. 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
λ
11. Scala
λ
object LambdaDemo extends App {
val numbers = List(1, 2, 3)
val numbersPlusOne =
numbers.map(number => number + 1)
}
Wednesday, November 6, 13
16. Not Reusable
• Lambdas are limited in scope to their call
site
• You cannot reuse the functionality
elsewhere
Wednesday, November 6, 13
17. 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
18. 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
19. 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
}}
}λ
}λ
}
λ
λ
λ
21. 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
22. 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
25. 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
26. Scala
val 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
27. Clojure
println(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
28. JRuby
array.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
29. 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
30. 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
31. 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
32. 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
33. 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
34. Named Function
object LambdaTest extends App {
val addOneToValue = (x: Int) => x + 1
}
val myList = (1 to 20).map(addOneToValue)
Wednesday, November 6, 13
35. Named Function Stack Trace
val badFunction = (x: Int) => x / 0
val 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
36. “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
37. Stack Trace of a Method
def badFunction = (x: Int) => x / 0
val 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
38. 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
•
Better to stick with simple method syntax instead
def badFunction(x: Int) = x / 0
Wednesday, November 6, 13
39. Stack Trace of a Stable Method
def badFunction(x: Int) = x / 0
val 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
40. Benefits
• You can’t close over variables
• Better stack traces
• More debuggable
• More testable
• More maintainable and descriptive
• Reusable
Wednesday, November 6, 13
41. Rule of Thumb
• Reserve lambda usage for the most basic
expressions
• Externalize anything more significant than
that to methods
Wednesday, November 6, 13
42. 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