Date post: | 21-Jan-2018 |
Category: |
Technology |
Upload: | shinolajla |
View: | 750 times |
Download: | 0 times |
I Love Functional Programming!
• Functional Programming is:
• Immutability
• Referential Transparency
• Functions as first-class citizens
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); }
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
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()); } }
λ
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));
λ
Scala
object LambdaDemo extends App { val numbers = List(1, 2, 3) val numbersPlusOne = numbers.map(number => number + 1) }
λ
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
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 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!
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
object LambdaTest extends App { val addOneToValue = (x: Int) => x + 1
val myList = (1 to 20).map(addOneToValue) }
Named Function Stack Traceval badFunction = (x: Int) => x / 0 val myList = (1 to 20).map(badFunction)
Exception in thread "main" java.lang.ArithmeticException: / by zeroat 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
“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 Methoddef badFunction = (x: Int) => x / 0 val myList = (1 to 20).map(badFunction)
Exception in thread "main" java.lang.ArithmeticException: / by zeroat 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 zeroat 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