Understanding the Building Blocks of FP
Dervis Mansuroglu
Functional Programming
In Java 22
JavaDay Istanbul 2024
Description:
Functional programming promises huge advantages, but it still remains a mystery to many. In this talk, I'll focus on explaining key concepts such as lazy, immutability, currying, composition, and also explain topics such as ADTs, morphisms, lambda calculus and advanced pattern matching in Java 22.
This talk is about the practical use of functional programming in an object-oriented language like Java. We know that it's very difficult to write pure code in a functional style in Java because the language lacks important elements such as immutability, laziness, higher order types, stronger type inference, better function composition and currying, tail-call optimization, and stronger pattern matching. These deficiencies lead to a lower degree of abstraction, poorer decomposition, and we cannot express ourselves as elegantly as we would like. So why should we care about functional programming in Java?
The answer lies in the fact that by learning techniques from other languages, you also become a better developer in the language you normally use. Java 21 now supports "Unnamed Patterns and Variables", and with the existing support for Sealed Interfaces and Record Patterns, some Java code can now be written much simpler and more expressively.
This is an informal look at functional programming, the aging java.util.function library, and the latest additions in JDK22 that bring Java closer to other JVM languages such as Kotlin. The talk is well suited for those who have not engaged much in functional programming, but who wish to get a bit of inspiration.
Bio:
Dervis is an experienced Java-developer and principal officer, currently working for the Norwegian Labour and Welfare Administration. He is passionate about programming languages, functional programming and algorithms. Dervis is a Java Champion and the leader of the Norwegian JUG JavaBin (Dukes Choice Award winner in 2019). Dervis has spoken at several international conferences as well as being a regular speaker at local meetups in Norway.
Principal Officer, developer, 15 years of exp.
Leader of the Norwegian JUG, javaBin
Who am I and why listen to me?
Oslo Software Architecture Co-Organizer
Java Champion
A Brief Summary
How Java is changing
Java is about clear, readable and reliable code.
Writing as less code as possible is not a goal itself for the Java programming language.
Make it easier to build and maintain reliable code ... that's why people continue to use and trust Java.
- Brian Goetz
Java adopts concepts from functional languages.
JEP-395 Records
JEP-360 Sealed Types
JEP-361 Switch Expressions
JEP-394 Pattern Matching, instanceof
JEP-441 Pattern Matching, switch
JEP-440 Record Patterns
JEP-443 Unnamed Patterns & Variables
Deconstructional pattern matching like in
Ref: https://javaalmanac.io/features/
Future features that enhances functional style in Java
JEP-468 Derived Record Creation
JEP-401 Value Classes
JEP-??? Null-Restricted Value Class Types
Ref: https://javaalmanac.io/features/
Important differences
Everything is functions.
Everything.
Lazy evaluation
Expression evaluation is delayed until needed. Its value remains constant between different points in time.
Immutability
An expression can be replaced with its value without changing the program's behavior.
Immutability can
exist without laziness
Laziness can't exist
without immutability
String
Records
Immutability + laziness allows algebraic reasoning & purity
Infinite sets & recursions
Evaluation vs. Effect
Value caching
Memory/CPU
+ referential transparency
Java's journey
Important changes in Java 7-22
Java's Generics
Introduced in Java 5 to allow generic types.
int[] arr = new int[]{ ... };
List<Integer> arr = new ArrayList<Integer>();
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {}
Vehicle[] busArray = new Bus[10];
busArray[0] = new Car(); // compiles :-(
List<Vehicle> list = new ArrayList<Bus>(); // error
Java's Diamond Operator
Introduced in Java 7 to allow type inference on generic types.
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {}
List<Vehicle> list = new ArrayList<>();
Java's var keyword
Introduced in Java 10 to allow type inference on local variables.
public void makeList() {
List<Vehicle> list = new ArrayList<>();
var list2 = new ArrayList<Vehicle>();
}
Java's intersection types with var
Introduced in Java 10 to allow type inference of multiple mix-in interfaces
var guest = (Welcome & Goodbye) () -> "World";
System.out.println(guest.welcome());
System.out.println(guest.goodbye());
>> Hello World
>> Goodbye World
interface Welcome extends Supplier<String> {
default String welcome() {
return String.format("Hello %s", get());
}
}
interface Goodbye extends Supplier<String> {
default String goodbye() {
return String.format("Goodbye %s", get());
}
}
Ref: https://javaalmanac.io/features/var/
Streams API & functions
Introduced in Java 8
Anno 2014
Functional Interfaces
- Function<T,R>
- BiFunction<T, U,R>
- Predicate<T>
- Supplier<T>
- Consumer<T>
- UnaryOperator<T> extends Function<T, T>
- BinaryOperator<T> extends BiFunction<T,T,T>
- BiPredicate<T>
- BiConsumer<T>
Function<Integer, Integer> f;
f = x -> x + 1;
private static Integer lambda$new$0(Integer);
0: aload_0
// Integer.intValue:()I
1: invokevirtual #24
4: iconst_1
5: iadd
// Integer.valueOf:(I)L/Integer;
6: invokestatic #30
9: areturn
Issues
- ADT's are not part of the language nor the library
- Was built before pattern matching, sealed interfaces, exhaustiveness and unnamed pattern variables.
- Certain function types do not build upon each other
- Example: Function vs BiFunction
- Example: Function vs BiFunction
- Java Stream pipeline api lacks intermediate functions
- Only extension point is custom Collectors
18.03.2014
19.09.2023
- Record Patterns (JEP 440, Java Almanac)
- Pattern Matching for switch (JEP 441)
- Unnamed Patterns and Variables (1st, JEP 443)
- Unnamed Classes and Instance Main Methods 1st, JEP 445)
15.03.2024
- Stream Gatherers 1. Preview (JEP 461)
19.09.2023
- Record Patterns (JEP 440, Java Almanac)
- Pattern Matching for switch (JEP 441)
- Unnamed Patterns and Variables (1st, JEP 443)
- Unnamed Classes and Instance Main Methods 1st, JEP 445)
15.03.2024
- Stream Gatherers 1. Preview (JEP 461)
18.03.2014
Aka ADT's
Algebraic Data Types
Sum type
This OR That.
Product type
This AND That
https://en.wikipedia.org/wiki/Tagged_union
Java records
Haskell data record types
Java classes
C structs
Tuples
https://en.wikipedia.org/wiki/Product_type
Java enums
Haskell data types (several constructors)
Also called: variant, choice type, disjoint union, coproduct
Sum type
This OR That.
Product type
This AND That
Records
Sealed classes & interfaces
Exhaustive Pattern Matching
Destructuring / Decomposition
Haskell
data Animal = Dog | Cat | Elk
data Animal = Dog String
| Cat String
| Elk String
name (Dog n) = n
name (Cat n) = n
name (Elk n) = n
Haskell
data Animal = Dog String
| Cat String Int
| Elk Int String Double
| Unknown
info (Dog n) = "Dog: " ++ n
info (Cat n _) = "Cat: " ++ n
info (Elk _ n _) = "Elk: " ++ n
info _ = "Unknown animal"
Haskell -
data Group = Mammal | Bird | Fish deriving (Show)
data Animal = Animal {
name :: String,
age :: Int,
group :: Group
}
elephant = (Animal "Elephant" 10 Mammal)
elephantName = name a
elephantAge = age a
elephantGroup = group a
Records
ADT's are patterns
Represents variants of data
A function can return either an error, or a result
A GPS-coordinate has a latitude and a longitude
A chess piece can be one of pawn, knight, bishop, rook etc.
A function might return a result or nothing
Maybe a = Just a | Nothing
Avoid null values. Alternative for Optional
Either a b = Left a | Right b
This or That (disjunction). Can be used to handle exceptions
These a b c = A | B | Both
This or That or Both. Choice type
Try a = Success a | Failure a
Functionally catch exceptions
ADT's with Java
Sealed types + Records
public sealed interface Maybe<T> permits Maybe.Just, Maybe.Nothing {
record Just<T>(T t) implements Maybe<T> {}
record Nothing<T>() implements Maybe<T> {}
static <T> Maybe<T> ofNullable(T value) {
return value != null ? new Just<>(value) : new Nothing<>();
}
default boolean isNothing() {
return this instanceof Maybe.Nothing<T>;
}
default boolean isJust() {
return this instanceof Maybe.Just<T>;
}
default <X> X maybe(Supplier<X> leftFn, Function<T, X> rightFn) {
return switch (this) {
case Nothing<T> _ -> leftFn.get();
case Just<T> left -> rightFn.apply(left.t());
};
}
}
This is only an approximation, not a full monad
Records
Immutable data classes with named accessors
Project Amber
Java -
enum Group { Mammal, Bird, Fish }
record Animal(String name, int age, Group group) {}
var elephant = new Animal("Elephant", 10, Mammal);
String name = elephant.name();
int age = elephant.age();
Type group = elephant.group();
Records
Sealed Types
A step closer to ADT's
Project Amber
public sealed interface Animal permits Cat, Dog {}
public record Cat() implements Animal {}
public record Dog() implements Animal {}
Sealed interface
public sealed abstract class Animal {}
public final class Cat extends Animal {}
public final class Dog extends Animal {}
Sealed class
public sealed interface Animal {
record Cat() implements Animal {}
record Dog() implements Animal {}
}
Sealed interface
public sealed interface Animal permits Cat, Dog {}
public record Cat(String name, int age) implements Animal {}
public record Dog(String name, int age) implements Animal {}
String afterJDK19(Animal animal) {
return switch (animal) {
case Dog d when d.age > 10 -> "Old dog: " + d.name();
case Dog d when d.age < 2 -> "Young dog: " + d.name();
case Dog d -> d.name();
case Cat c -> c.name();
};
}
public sealed interface Animal permits Cat, Dog {}
public record Cat(String name, int age) implements Animal {}
public record Dog(String name, int age) implements Animal {}
String beforeJDK19(Animal animal) {
return switch (animal) {
case (Dog d && d.age > 10) -> "Old dog: " + d.name();
case (Dog d && d.age < 2) -> "Young dog: " + d.name();
case Dog d -> d.name();
case Cat c -> c.name();
};
}
public sealed interface Animal permits Cat, Dog {}
public record Cat(String name, int age) implements Animal {}
public record Dog(String name, int age) implements Animal {}
String fromJDK21(Animal animal) {
return switch (animal) {
case Cat(var n, var a) -> "Cat name = %s, age = %s".formatted(n, a);
case Dog(var n, var a) -> "Dog name = %s, age = %s".formatted(n, a);
case null -> "Invalid animal";
};
}
// Record Patterns
public sealed interface Animal permits Cat, Dog {}
public record Cat(String name, int age) implements Animal {}
public record Dog(String name, int age) implements Animal {}
String fromJDK21(Animal animal) {
return switch (animal) {
case Cat(var _, var a) -> "Cat age %s".formatted(a);
case Dog(var n, _) when n.isEmpty() -> "Dog without name";
case Dog(_, var a) when a > 10 -> "An old Dog";
case Dog(_, _) -> "Unknown Dog";
case null -> "Invalid animal";
};
}
// Unnamed patterns and variables
public sealed interface Either<L, R> permits Either.Left, Either.Right {
record Left<L, R>(L l) implements Either<L, R> { }
record Right<L, R>(R r) implements Either<L, R> { }
static <L, R> Left<L, R> left(L l) {
return new Left<>(l);
}
static <L, R> Right<L, R> right(R r) {
return new Right<>(r);
}
default boolean isRight() {
return this instanceof Either.Right<?, R>;
}
default L left() { return ((Left<L, ?>)this).l(); }
default R right() { return ((Right<?, R>)this).r(); }
default <X> X either(Function<L, X> leftFn, Function<R, X> rightFn) {
return switch (this) {
case Either.Left<L, ?> left -> leftFn.apply(left.l());
case Either.Right<?, R> right -> rightFn.apply(right.r());
};
}
}
This is only an approximation, not a full monad
Terminology in FP
A short recap
The Haskell Book
A function is a relation between
a set of possible inputs and a
set of possible outputs.
The function itself defines and
represents that relationship.
Haskell In Depth:
- Composition of functions
- Pure functions
- Referential transparency
- Equational reasoning
- Absence of side effects
- Abstraction
Abstraction
⭐️ Abstraction hides complex implementation details behind a simpler, higher-level interface.
⭐️ Abstraction is generalised representations of patterns, structures, properties or behaviours, and making it more widely applicable.
Publication -> Newspaper -> Aftenposten -> 11th May Aftenposten
Abstraction
⭐️ Higher level of abstraction means highly expressive and less verbose code.
⭐️ But be aware of "The Law of Leaky Abstractions"
⭐️ Higher level of abstraction can also lead to higher level of complexity.
𝜆𝑥.𝑥+1
𝜆𝑥𝑦.2𝑥+𝑦
𝜆𝑥.(𝜆𝑦.𝑥+𝑦)
CLOSED
OPEN
Combinator
CLOSED
CLOSED/closure
Free variable
In calculus, abstractions is represented by lambda expressions.
evens numbers = [x | x <- numbers, x `mod` 2 == 0]
evens numbers = filter even numbers
Haskell
List<Integer> filterEven(List<Integer> numbers) {
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
return evenNumbers;
}
numbers.stream().filter(n -> n % 2 == 0).toList()
Java
public Maybe<Pair<Year, Salary>> min(Person person) {
Salary minSalary = null;
Year minYear = null;
for (Salary s : person.incomeHistory)
if (minSalary == null || isLowerSalary(s, minSalary)) {
minSalary = s;
minYear = s.year();
}
if (minSalary != null) {
return Maybe.of(new Pair<>(minYear, minSalary));
} else
return new Maybe.Nothing<>();
}
boolean isLowerSalary(Salary salary1, Salary salary2) {
return salary1.salary().compareTo(salary2.salary()) < 0;
}
Function<Person,
Maybe<Pair<Year, Salary>>> min =
person -> person.incomeHistory.stream()
.min(Comparator.comparing(Salary::salary))
.map(s -> new Pair<>(s.year(), s))
.map(Maybe::ofNullable)
.orElse(new Maybe.Nothing<>());
Expressions
Expressions may be values, variables, combinations of values and expressions, or functions applied to values.
As in mathematics, expressions and functions must always return a value, and the same value given the same input.
Purity
Pure functions are referentially transparent, meaning a function always returns the same result with the same inputs (hence you can replace the function with its value).
- Purity also means no side-effects (mutation of externally visible data, and no shared-mutability).
Idempotent
Reduction
Reduction is the process of applying a function to an argument and simplifying the resulting expression.
Substitution
Substitution is the process of applying variables with their corresponding values in a function expression.
Expressions in normal form have no more evaluation steps (aka irreducible form)
Combining Functions
-
Curry
-
Partial Application
-
Higher-Order Functions
-
Composition
curried functions
converting function of \(n\) arguments into \(n\) functions with a single argument each
interface IntFunction extends Function<Integer, Integer>{}
Function<Integer, IntFunction> f = x -> (y -> x + y);
println( f.apply(3).apply(4) );
a function is partially applied if some of its arguments are not set
partially applied functions
interface DoubleFunction extends Function<Double, Double> {}
Function<Double, DoubleFunction> sum = x -> (y -> x * y);
Function<Double, Double> num66 = sum.apply(0.66);
Function<Double, Double> calc = num66.andThen(a -> a / 3);
var result = calc.apply(50);
num :: (Fractional a) => a -> a -> a
num x y = x * y
f66 :: Double -> Double
f66 = num 0.66
calc :: Double -> Double
calc x = (/) (f66 x) 3
interface DoubleFunction extends Function<Double, Double> {}
Function<Double, DoubleFunction> sum = x -> (y -> x * y);
DoubleFunction num66 = sum.apply(0.66);
DoubleFunction calc = num66.andThen(a -> a / 3);
var result = calc.apply(50);
higher order functions
functions that take other functions as parameter, and/or returns a function
BiFunction<Person, Function<Person, String>, String>
map = (person, fn) -> fn.apply(person);
<T, R> Supplier<R> lazyMap(T p, Function<T, R> fn) {
return () -> fn.apply(p);
}
composition
mapping output of one function to the input of another, creating a new function
$$ rs = (reverse~.~sort)$$
composition
enum VehicleType {ANY, BUS, SCHOOL};
Function<List<? super Bus>, List<? extends String>> classNames ...
Function<VehicleType, List<? super Bus>>
make = n -> switch (n) {
case BUS -> List.of(...);
case SCHOOL -> List.of(...);
case ANY -> List.of(...);
};
Function<List<String>, List<String>>
toString = s -> s.stream().map(String::toUpperCase).toList();
classNames.andThen(toString).compose(make).apply(VehicleType.BUS);
1
2
3
4
Understanding the order of execution is difficult in Java.
toString( classNames( make( BUS ) ) )
Text
Morphism
Transforming shapes
Morphism is a general term that refers to the transformation or mapping between objects or structures.
Why talk about this?
-
Operations on data affect logical consistency and predictable transformations of data structures (behaviour)
- Using proper data types and operations in a way that respects the data's intended interpretation
- Some people just have a buzzword-fetish making learning harder to beginners 😔🤯
Polymorphism
A fundamental principle in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass.
sealed interface Animal permits Dog, Cat {}
interface Dog extends Animal {}
interface Cat extends Animal {}
Function<Animal, String> toString
Allows flexible and reusable code by enabling objects of different classes to interact through common interfaces
Polymorphism
Static/Compile-time polymorphism (aka method overloading)
void main() {
sum(1d, 1d);
}
int sum(int a, int b) {
return a + b;
}
long sum(long a, long b) {
return a + b;
}
Double sum(Double a, Double b) {
return a + b;
}
Polymorphism
Dynamic/Run-time polymorphism (aka method overriding)
class Shape {
String info() {
return "This is a shape";
}
}
class Circle extends Shape {
public String info() {
return "This is a circle";
}
}
void main() {
Shape circle = new Circle(); // upcasting
circle.info();
}
Polymorphism
Some issues to be aware of...
Object getSomeObject() {
return new Object();
}
class Circle extends Shape {
public String info() {
return "This is a circle";
}
}
void main() {
Object o = getSomeObject();
String s = (String) o; // compiles :(
Circle c = (Circle) new Shape(); // compiles :(
}
Monomorphism
A function is monomorphic if it works only for one type
sealed interface Animal permits Dog, Cat {}
interface Dog extends Animal {}
interface Cat extends Animal {}
// monomorphic functions
Function<Cat, Cat> copyCat
Function<Dog, Dog> updateDog
// polymorphic
Function<Animal, String> name
Homomorphism
A function is homomorphic if it preserves structure when mapping
// Composition law: f(a + b) = f(a) + f(b)
record Price(double amount) { }
Function<Price, Price> zam =
p -> price(p.amount() * 1.10); // 10% zam
zam(price(50)) == zam(price(25)) + zam(price(25))
Price price(double amount) {
return new Price(amount);
}
// Composition law: f(a + b) = f(a) + f(b)
record Price(String amount) {...}
Function<Price, Price> zam =
p -> price(""+parseDouble(p.amount()) * 1.10);
zam(price("50")) ==> "55"
zam(price("25")) + zam(price("25")) ==> "27.027.0"
Price price(String amount) {
return new Price(amount);
}
Not homomorphic. Arithmetics is confused with concatenation.
Isomorphism
Isomorphism is a type of relation where two structures can be mapped to each without loss of data (aka preserving all properties)
record Point(int x, int y) {}
record Tuple(int _1, int _2) {}
Function<Point, Tuple> toTuple = p -> new Tuple(p.x, p.y)
Function<Tuple, Point> toPoint = t -> new Point(t._1, t._2)
Variance
Variance refers to how subtyping between more complex types relates to subtyping between their components. (Wikipedia)
Invariance
Invariance does not allow subtypes or supertypes.
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {}
// error - list is invariant
List<Vehicle> list = new ArrayList<Bus>();
List<Vehicle> list = new ArrayList<Object>();
// ok - the exacte type
List<Vehicle> list = new ArrayList<Vehicle>();
You can read and write to the list.
Contravariance
Contravariance allows supertypes of the specified type.
class Vehicle {}
class Bus extends Vehicle {}
class SchoolBus extends Bus {}
// error - list is contravariant
List<? super Bus> list = new ArrayList<SchoolBus>();
// ok - supertypes of Bus
List<? super Bus> list = new ArrayList<Vehicle>();
List<? super Bus> list = new ArrayList<Object>();
Covariance
Covariance allows subtypes of the specified type.
class Vehicle {}
class Bus extends Vehicle {}
class SchoolBus extends Bus {}
// ok - list is covariant
List<? extends Bus> list = new ArrayList<Bus>();
List<? extends Bus> list = new ArrayList<SchoolBus>();
// error - these are supertypes of Bus
List<? extends Bus> list = new ArrayList<Vehicle>();
List<? extends Bus> list = new ArrayList<Object>();
Data Modelling
How do we *think* with FP?
Data
Data holds information about the context we would like to represent in our model.
Operations
Operations process and transform the data in our model.
We represent our data and the model as values.
We represent the operations in our application as functions.
You describe your set of values (your data)
You describe operations that work on those values (your functions)
1)
2)
Source: https://docs.scala-lang.org/scala3/book/domain-modeling-fp.html
Object-orientated
Data
Operations
class Person {
private Name name;
private Address address;
private List<Salary> salaries;
Name getName() {}
Address getAddress() {}
SalaryData foldSalaries() {}
Pair<Year, Salary> max() {}
}
Object-orientated
Data
Operations
Data
Operations
Functional
Object-orientated
Classes, Objects, Methods, Fields, Constructors
Modules, Types Classes, Values, Functions
Data
Operations
Data
Operations
data Name = Name String String
type Address = String
type Year = Int
type Month = Int
type Amount = Double
data Salary = Year Month Amount
data SalaryList = SalaryList [Salary]
data Person = Name Address SalaryList
personName :: Person -> String
personAddress :: Person -> String
fold :: [Salary] -> SalaryData
max :: [Salary] -> (Year, Salary)
min :: [Salary] -> (Year, Salary)
Functional
-- Fold function to process a list of Salaries
fold :: [Salary] -> SalaryData
fold salaries =
let total = sum [amount | Salary _ _ amount <- salaries]
count = fromIntegral (length salaries)
average = if count > 0 then total / count else 0
in SalaryData total average
// pure
// immutable
Modules, Types Classes, Values, Functions
Data
record Name(String fname, String lname) {}
record Address(String address) {}
record Year(Integer year) {}
record Month(Integer month) {}
record Amount(Double amount) {}
record Salary(Year y, Month m, Amount a) {}
record SalaryList(List<Salary> salaries) {}
record Person(Name n, Address a, SalaryList s) {}
data Name = Name String String
type Address = String
type Year = Int
type Month = Int
type Amount = Double
data Salary = Year Month Amount
data SalaryList = SalaryList [Salary]
data Person = Name Address SalaryList
Modules, Types Classes, Values, Functions
Operations
Function<Person, String> name
Function<Person, String> addr
Function<List<Salary>, SalaryData> fold
Function<List<Salary>, Pair<Year, Salary>> max
name :: Person -> String
addr :: Address -> String
fold :: [Salary] -> SalaryData
max :: [Salary] -> (Year, Salary)
interface DoubleFunction extends Function<Double, Double> {}
interface F3 extends Function<Double, Function<Double, DoubleFunction>> {};
F3 fn = a -> b -> c -> a + b + c;
public class PersonFP{
public record Person(Name name, Address address,
List<Salary> incomeHistory) {}
public record Name(String fullName) {}
public record Address(String street, String city, String country) {}
public record Salary(Year year, Month month, Double salary) {}
public static Function<Person, Maybe<Double>> foldSalaries =
person -> person.incomeHistory.stream()
.map(Salary::salary)
.reduce(Double::sum)
.map(Maybe::ofNullable)
.orElse(new Maybe.Nothing<>());
public static Function<Person, Maybe<Pair<Year, Salary>>> min =
person -> person.incomeHistory.stream()
.min(Comparator.comparing(Salary::salary))
.map(s -> new Pair<>(s.year(), s))
.map(Maybe::ofNullable)
.orElse(new Maybe.Nothing<>());
public static Function<Person, Maybe<Pair<Year, Salary>>> max =
person -> person.incomeHistory.stream()
.max(Comparator.comparing(Salary::salary))
.map(s -> new Pair<>(s.year(), s))
.map(Maybe::ofNullable)
.orElse(new Maybe.Nothing<>());
}
Purely functional code in Java:
No side-effects or shared/visible mutability
Immutable data are possible
Functional style, expressive code
Pure functions
Algebraic Data Types (product types)
No nulls, predictable behaviour
(with equational reasoning, but there many pitfalls, such when using parallelsim )
What are the issues:
No lazy or immutability out-of-box
Composition is hard and verbose
Functional style takes time to learn
Difficult to keep overview as code base grows
Java is too verbose
Added complexity & code to maintain (ie: Either)
Weak type inference compared to Haskell
Hard to deal with side-effects in a pure style
What have I learned?
-
Declare functions
-
Compose several functions to bigger ones
-
Contain and handle side-effects in monads
-
Use pattern matching and ADTs
-
Create immutable data structures
-
Combine streams and functions
Summary - The good stuff
- Pattern matching for switch are great
- Exhaustiveness through sealed classes are great!
- Deconstructional pattern matching is great!
- Unnamed patterns and variables are finally here
- Records as (shallowly) immutable data types is good
- Streams are lazy - and supports infinite sets
- Functional style with the java.util.function interfaces can have its advantages if used wisely.
The journey
- Getting used to everything being functions
- Relearning the theory about object orientation
- Relearning concepts from mathematics
- Learning a completely different paradigm
- Many questions: state, io, loops, recursion etc
- Learning a huge library of functions
- Learning what and how to compose functions
- Learning concepts from category theory
@dervismn
Thanks!
https://bit.ly/javaday24
linkedin.com/in/dervism/