Functional Programming [introduction] Félix-Étienne Trépanier
Functional Programming
[introduction]
Félix-Étienne Trépanier
Call me Félix- Software Engineer - Independant- Code Java / Scala (mainly)- Build Distributed Systems- Organizer of Scala-Montreal
Meetups
Agenda- Motivations- Definitions and Examples
Motivations
What makes programming so hard?
“Controlling complexity is the essence of computer programming.”- Brian Kernighan
Complexity comes from input and state.
Possible Inputs
Possible States
Possible OutcomesX =
We should aim at reducing the input and state space.
Possible Inputs
Possible States
Possible OutcomesX =
In Object-Oriented programming, everything* is an object.
Objects combine state and behavior.
Objects are state machines.
A = xB = y
A = x’B = y’
A = x’B = y
A = xB = y’
Most objects are badly designed state machines.
A = xB = y
A = x’B = y’
A = x’B = y
A = xB = y’
Large state space are hard to reason about.
Concurrency adds new visible states.
A = xB = y
A = x’B = y’
A = x’B = y
A = xB = y’
How can we write correct code if we can’t reason about it?
Let’s add more unit tests!
Let’s add more unit tests!
Definitionsand
Examples
Functional Programming imposes constraints that eliminate states and ease reasoning.
Functional Programming is about values, functions and types*.
Values
Values are immutable.
Example - Javafinal String title = "functional programming";
final Person bob = new Person("bob", 34);
class Person {
private final String name;
private final int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
}
...
}
Example - Scalaval title = "functional programming"
val bob = User("bob", 34)
case class User(name: String, age: Int)
What about data structures?Can they be values?
Persistent data structures create a new updated version. They are immutable.
Example - Java (1)// using functional java
import fj.data.List;
final List<String> canadianCities =
List.list("Montreal", "Ottawa", "Toronto");
final List<String> americanCities =
List.list("New York", "San Francisco");
final List<String> northAmericanCities =
canadianCities.append(americanCities);
Example - Java (2)// using guava
final ImmutableList<String> canadianCities =
ImmutableList.of("Montreal", "Ottawa", "Toronto");
final ImmutableList<String> americanCities =
ImmutableList.of("New York", "San Francisco");
final ImmutableList<String> northAmericanCities =
ImmutableList.<String>builder().addAll(canadianCities)
.addAll(americanCities).build();
// or
final ImmutableList<String> northAmericanCities =
Stream.concat(canadianCities.stream(), americanCities.stream())
.collect(GuavaCollectors.immutableList());
Example - Scalaval canadianCities = List("Montreal", "Ottawa", "Toronto")
val americanCities = List("New York", "San Francisco")
val northAmericanCities = canadianCities ++ americanCities
Immutability eliminates state and means you can safely share the reference.
Functions
A function takes arguments and produces a result.
(Pure) Functionshave no side-effects.
Side-effects examples:- write to disk- read from stdin- throw an exception- change a state
Example - Java// pure function
public static String convert(String input) {
return new StringBuilder(input).reverse()
.toString().toUpperCase();
}
// unpure function
public static void printMessage(String message) {
if(message == null) {
throw new IllegalStateException("Should not be null");
}
System.out.println(message);
}
Example - Scala// pure function
def convert(a: String): String = {
a.reverse.toUpperCase
}
// unpure function
def print(message: String): Unit = {
if (message == null) {
throw new IllegalStateException("Should not be null!")
}
println(message)
}
Having no side-effecthelps reasoning about code.
Having no side-effect simplifies testing.
Errors are handled by returning them as results.
Example - Javapublic static Optional<Integer> parseInt(String s) {...}
// using functional java
public static Either<Exception, Integer> parse(String s) {...}
// Use combinators to avoid branching
final double discount = parseInt(ageAsString)
.map(a -> a * 0.01).orElse(0.10);
// Assume Optional<Integer> age1, age2;
final Optional<Integer> sum = age1.flatMap(a1 ->
age2.map(a2 ->
a1 + a2));
Example - Scaladef parseInt(s: String): Option[Int] = {...}
val sum: Option[Int] = for {
a <- parseInt("2")
b <- parseInt("3")
} yield a + b
Updating a state is done by creating a new instance with the updated state.
For other necessary side-effects, push them on the boundaries.
Looping can be implemented with recursion*.
Example - Scaladef sumAllFrom(value: Int): Int = {
// always use the @tailrec annotation
// tail recursive functions can be optimized to a loop
@tailrec
def internalSum(value: Int, currentSum: Int): Int = {
if (value == 0) {
currentSum
} else {
internalSum(value - 1, currentSum + value)
}
}
internalSum(value, 0)
}
A high-order functions takes functions as parameters and/or produces a function.
Example - Javafinal ImmutableList<Integer> l = ImmutableList.of(1, 2, 3, 4);
final ImmutableList<Integer> mapped = l.stream()
.filter(e -> (e % 2) == 0)
.map(e -> e * 2)
.collect(GuavaCollectors.immutableList());
Example - Scalaval l = List(1, 2, 3, 4)
val mapped = l.filter(e => e % 2 == 0).map(_ * 2)
val mappedStream = l.toStream
.filter(e => e % 2 == 0)
.map(_ * 2)
.toList
Example - Javafinal Function<Integer, Integer> doubled = e -> e * 2;
final Function<Integer, Integer> increment = e -> e + 1;
final Function<Integer, Integer> doubleAndIncrement =
doubled.andThen(increment);
Types
Types define classifications of values.
With types, the compilercan verify some aspects of correctness.
Example - Scalacase class User(name: String, city: String, phoneNumber: String)
// ?!
val user = User("Montreal", "John", "New York")
// These types could include validation
// so any object successfully created is valid.
// Check value classes to avoid garbage
trait Name {...}
trait City {...}
trait PhoneNumber {...}
case class User(name: Name, city: City, phoneNumber:PhoneNumber)
Meta
Object-Oriented is about semantic.
Functional Programming is about shape.
Example - Javainterface Process
interface ProcessConfig
interface ProcessFactory {
Process createProcess(ProcessConfig config)
}
interface Machine
interface MachineConfig
interface MachineFactory {
Machine createMachine(MachineConfig config)
}
Example - Javainterface Process
interface ProcessConfig
interface Machine
interface MachineConfig
interface Factory<T, C> {
T create(C config)
}
interface ProcessFactory extends Factory<Process, ProcessConfig>
interface MachineFactory extends Factory<Machine, MachineConfig>
Example - Javainterface Process
interface ProcessConfig
interface Machine
interface MachineConfig
interface Factory<T, C> {
T create(C config)
}
interface ProcessFactory extends Factory<Process, ProcessConfig>
interface MachineFactory extends Factory<Machine, MachineConfig>
Function<C, T>
Reuse by shape is more powerful than reuse by semantic.
BUT understanding shape is harder than understanding semantic.
Summary
Summary
- Functional programing: immutable values, pure functions and precise types*.
- Benefits are:- Simple code- Concurrency and Parallelism for free- Easy to reason about- Reusable
ContactFélix-Étienne Trépanier
Twitter - @felixtrepanierGithub - @coderunner
unconfoo session @ 2pm!
Imageshttps://www.flickr.com/photos/vestman/https://www.flickr.com/photos/arenamontanus/