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

  1. Function<T,R>
  2. BiFunction<T, U,R>
  3. Predicate<T>
  4. Supplier<T>
  5. Consumer<T>
  6. UnaryOperator<T> extends Function<T, T>
  7. BinaryOperator<T> extends BiFunction<T,T,T>
  8. BiPredicate<T>
  9. 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
       
  • Java Stream pipeline api lacks intermediate functions
    • Only extension point is custom Collectors

18.03.2014

  • Lambda Expressions (JSR 335)
  • Default Methods in Interfaces (JSR 335)
  • Streams (java.util.stream) (JEP 107)
  • Lambda APIs (java.util.function) (JEP 109)
  • Date Time (java.time, aka joda-time) (JEP 150)

19.09.2023

15.03.2024

  • Stream Gatherers 1. Preview (JEP 461)

19.09.2023

15.03.2024

  • Stream Gatherers 1. Preview (JEP 461)

18.03.2014

  • Lambda Expressions (JSR 335)
  • Default Methods in Interfaces (JSR 335)
  • Streams (java.util.stream) (JEP 107)
  • Lambda APIs (java.util.function) (JEP 109)
  • Date Time (java.time, aka joda-time) (JEP 150)

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).

f(1) = X \\ f(1) = Y \\ f(2) = Z
f(1) = A \\ f(2) = B \\ f(3) = C

Idempotent

Reduction

Reduction is the process of applying a function to an argument and simplifying the resulting expression.

8 + 2 * 5 - 3 / 6 \\ 8 + (2 * 5) - (3 / 6) \\ 8 + 10 - 0.5 \\ (8 + 10) - 0.5 \\ 18 - 0.5 \\ 17.5 \\

Substitution

Substitution is the process of applying variables with their corresponding values in a function expression.

𝑓(𝑥) = 𝑥 + 1 \\ 𝑓 = 1 + 1 \\ 1+1\\ 2
𝑓(𝑥, y) = 2𝑥y\\ x = a\\ f(y) = 2ay\\ y = b\\ 2ab

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

sum = (x, y) \to x + y
sum = x \to y \to x + y
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)$$ 

rs~[3,1,5,2,4]
= [5, 4, 3, 2, 1]

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?

  1. Operations on data affect logical consistency and predictable transformations of data structures (behaviour)
     
  2. Using proper data types  and operations in a way that respects the data's intended interpretation
     
  3. 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/