OCP Review 2.2 - Functional Programming intro
Functional Programming
- no side effects
- data in, data out
- no mutability (no state)
- state is bad
- shared mutable state is the root of all evil
Functional Interface
- an interface with just one method
interface Add {
int add(int n1, int n2);
}
Functional Interface
- we can add the annotation
@FunctionalInterface
@FunctionalInterface
interface Add {
int add(int n1, int n2);
}
@FunctionalInterface
interface Substract {
int substract(int n1, int n2);
}
Just one method, please
@FunctionalInterface
interface Add {
int add(int n1, int n2);
void m1(); // wrong: two methods
}
@FunctionalInterface
interface Add {
int add(int n1, int n2);
static void m1(); // right: just one method
}
No Sh*t, sherlock, just one method
@FunctionalInterface
interface Add {
int add(int n1, int n2);
}
@FunctionalInterface
interface Substract {
int substract(int n1, int n2);
}
@FunctionalInterface // ERROR
interface Operation extends Add, Substract {
// add & substract are copied here
}
Lambda expressions
- a way to consume functional interfaces
- a way to implement those interfaces
(String instance) -> { return instance.legth(); }
// Lambda expression for a Functional Interface
// that declares a method taking one input param of type String, and returns an int
// shorthand
s -> s.length;
Lambda expressions
- can omit () if no type present and just one argument
- can omit
return
and;
Example: calculator
- a calculator can +, -, *, /, %, …
- restricted (for now) to
int
type - we can use an enum for operations and a Switch:
class OldCalculator {
enum OldOperations {
ADD, SUBSTRACT, MULTIPLY, DIVIDE
}
int calculate(int n1, int n2, OldOperations operation) {
int result = 0;
switch (operation) {
case ADD:
result = n1 + n2; break;
case SUBSTRACT:
result = n1 - n2; break;
case MULTIPLY:
result = n1 * n2; break;
case DIVIDE:
result = n1 / n2; break;
}
return result;
}
}
OldCalculator oldCalculator = new OldCalculator();
int r = oldCalculator.calculate(1,2, OldCalculator.OldOperations.ADD);
System.out.println("r: " + r);
Example: calculator
- problem: we want to add another operation
- change the
enum
- touch the
switch
- recompile
- if code is copied in other projects…
- change the
Example: calculator
- Functional solution: all these are arithmetic operations between two numbers. Let’s say that:
@FunctionalInterface
interface Operation {
int operate(int n1, int n2);
}
- and create a
Calculator
class with a method that takes two numbers and anOperation
- as all
Operation
references havecalculate
we can call it
class Calculator {
int calculate(int n1, int n2, Operation op) {
return op.operate(n1, n2);
}
}
Example: calculator
- using it: we’re passing two numbers and what we want to do with these two numbers (the code block)
Calculator c = new Calculator();
int i = c.calculate(1, 2, (n1, n2) -> n1 * n2);
i = c.calculate(2, 2, (int n1, int n2) -> n1 * n2);
i = c.calculate(2, 2, (int n1, int n2) -> n1 * n2);
i = c.calculate(2, 2, (int n1, int n2) -> { return n1 * n2; });
Example: calculator
- Lambda expressions can be passed around, or stored…
final Operation multiply = (n1, n2) -> n1 * n2;
so we can do:
class Calculator {
static final Operation multiply = (n1, n2) -> n1 * n2;
static final Operation add = (n1, n2) -> n1 + n2;
static final Operation substract = (n1, n2) -> n1 - n2;
static final Operation divide = (n1, n2) -> n1 / n2;
int calculate(int n1, int n2, Operation op) {
return op.operate(n1, n2);
}
}
// using it
i = c.calculate(40, 40, Calculator.divide);
Example: calculator
- we can return a Lambda expression from a method
Operation sum() {
return ((n1, n2) -> n1 + n2);
}
Example: calculator
- even return a random operation each time
Operation randomOperation() {
Operation[] operations = {
multiply, add, substract, divide
};
Random r = new Random();
return operations[abs(r.nextInt()) % operations.length];
}
Write correct Functional Interfaces for the following Lambda Expressions
() -> 10;
(a) -> a.size();
Answers
L1 l1 = () -> 10;
interface L1 {
int m();
}
L3 l3 = a -> a.size();
@FunctionalInterface
interface L3 {
int m(Collection c);
}
Write correct Functional Interfaces for the following Lambda Expressions
(a, b, c) -> a.size() + b.length() + c.length;
##
interface L4 {
int m(Collection a, String b, Object[] c);
}
Write correct Functional Interfaces for the following Lambda Expressions
() -> () -> 10;
L2 = () -> () -> 10;
interface L2 {
L1 m();
}
Useful lambdas
- Without lambdas…
Runnable task1 = new Runnable() {
@Override
public void run() {
System.out.println("Something!");
}
};
task1.run();
- With Lambdas…
Runnable task2 = () -> { System.out.println("Yeah!"); };
task2.run();
Useful lambdas: repeating codeblocks
interface CodeBlock {
void execute();
}
CodeBlock cb = () -> {
System.out.println("Hello");
};
cb.execute();
cb.execute();
Useful lambdas: forEach
List<String> streets = Arrays.asList("Torricelli", "Sierpes", "Constitución");
for (String street : streets) {
System.out.println(street);
}
streets.forEach((String x) -> {System.out.println(x);});
streets.forEach((String x) -> System.out.println(x));
streets.forEach(x -> System.out.println(x));
streets.forEach(System.out::println);
Lambdas capture the semantic scope
int i = 4;
CodeBlock cb = () -> {
System.out.println("Hello " + i);
};
cb.execute();
Final vs EffectivelyFinal
final
: a constant with a explicitfinal
keyword. Can’t change after being set- Effectively final: the compiler knows you have defined a variable, but never changed it (usually, all method parameters). We can use it from a lamda expression without the need to mark it
final
- but we can’t change the value of that constant
- it’s a constant, but now we don’t have to explicitly mark it as
final