what you need to know about lambdas
DESCRIPTION
What You Need to Know about Lambdas - the problem with lambdas (as in anonymous functions) and the way to solve those problems (hint - using methods lifted to functions).TRANSCRIPT
@knight_cloud
What You Need to Know About Lambdas
Ryan KnightSenior Consultant | Typesafe
Acknowledgement
• Material originally created by Jamie Allen
• @jamie_allen
• Modified by Ryan Knight
• @knight_cloud
I Love Functional Programming!
• Functional Programming is:
• Immutability
• Referential Transparency
• Functions as first-class citizens
• Eliminating side effects
Why Functional Programming?
• Foundation for Reactive Programming
• Requires Callbacks to handle Events and Async Results
• Avoid Inner Classes
• Higher-Level of Abstraction
• Define the What not the How
Imperative 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); }
We Want Declarative Code
• Remove temporary lists - List<Integer> numbersPlusOne = Collections.emptyList();
• Remove looping - for (Integer number : numbers)
• Focus on what - x+1
What is a Lambda?
• A function literal - fixed value in the code.
• 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
Java 8 Lambdas
• Functional Interface that defines a Single Abstract Method
• Target Type of the Lambda is the Functional Interface
• java.util.function - Package with standard Functional Interfaces
Java 8 Function Example@FunctionalInterface public interface Function<T, R> { ! /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t);
Target Type of Lambda
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()); } }
λ
Scala
• Have a Function Type used to represent the Lambda
• map(f:Integer => Integer)
• Creates a Function underneath the covers
Scala
object LambdaDemo extends App { val numbers = List(1, 2, 3) val numbersPlusOne = numbers.map(number => number + 1) }
λ
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));
λ
Clojure
(ns LambdaDemo.core) (defn -main [& args] (println(map #(+ % 1) [1, 2, 3])))
λ
JRuby
require "java" !array = [1, 2, 3] array.collect! do |n| n + 1 end
λ
What is the Problem?
There Are Caveats
Not Reusable
• Lambdas are limited in scope to their call site
• You cannot reuse the functionality elsewhere
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
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
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)
}
} }}}
λλλλλ
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
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
Nashorn Javascript
list.parallelStream(). map(function(e) e / 0) ![1, 2, 3] Infinity,Infinity
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
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
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
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
Difficult Debugging
• Debuggers on the JVM are getting better - still hard to debug Lambdas
final List<Integer> numbersPlusOne = numbers.stream(). map(number -> number + 1).collect(Collectors.toList());
NO!
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());
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)
Solution
We want to maintain our ability to program in a functional style, while having something
maintainable and understandable in production
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
Named Function
final Function<Integer, Integer> addOneToValue = number -> number / 0; !final List<Integer> numbersPlusOne = numbers.stream() .map(addOneToValue) .collect(Collectors.toList());
Named Function Stack Tracefinal Function<Integer, Integer> addOneToValue = number -> number / 0; !final List<Integer> numbersPlusOne = numbers.stream().map(addOneToValue) .collect(Collectors.toList()); !!Exception in thread "main" java.lang.ArithmeticException: / by zero at demoFuncs.BadExamples.lambda$main$0(BadExamples.java:15) at demoFuncs.BadExamples$$Lambda$1/1044036744.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:499) at demoFuncs.BadExamples.main(BadExamples.java:20)
wat
“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
Stack Trace of a Method Java 8
public static Integer addOneToValue(Integer number) { return number / 0; } !final List<Integer> numbersPlusOne = numbers.stream() .map(BadExamples::addOneToValue) .collect(Collectors.toList()); !Exception in thread "main" java.lang.ArithmeticException: / by zero at demoFuncs.BadExamples.addOneToValue(BadExamples.java:12) at demoFuncs.BadExamples$$Lambda$1/1826771953.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:499) at demoFuncs.BadExamples.main(BadExamples.java:23)
Much Better!
Stack Trace of a Method Scala
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
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
Stack Trace of a Stable Methoddef 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!
Benefits• You can’t close over variables
• Better stack traces
• More debuggable
• More testable
• More maintainable and descriptive
• Reusable
Rule of Thumb
• Reserve lambda usage for the most basic expressions
• Externalize anything more significant than that to methods
Java 8 Futures
final CompletableFuture<Object> cacheFuture = CompletableFuture .supplyAsync(() -> { return cacheRetriever.getCustomer(id); }); !cacheFuture.thenApply(customer -> dbService.getOrders(customer))
• java.util.concurrent.CompletableFuture
• Future operation specified as a Function
• Callback specified as Function when the Future completes
Java 8 vs. Scala• Scala Functions are a first-class citizens
• Scala compiler can infer type of Function
• Java 8 Lambdas mapped to Functional Interfaces
• Weaker Type Inference - Can only infer type based on left hand side of assignment operator
Java 8 vs. Scala
• Scala still targeting Java 1.6
• Creates synthetic class to represent Lambda
• Java 8 Lambdas leverage invoke dynamic
• Avoids creating synthetic classes
• http://www.takipiblog.com/2014/01/16/compiling-lambda-expressions-scala-vs-java-8/
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 - Smart stepping into Lambdas
• IntelliJ - Smart Step Into
Thank You!
• Questions?