Pure functions, High Order functions, Immutability, Composition, Currying, Recursion, Lazy, Functors, Foldable, Applicative, Category, Semi Group, Monoid, Monad.
Concepts
I won’t write something easier to follow than: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
I encourage you to follow it, but for this article I will take some of its examples and just show you how you can do it in Java with Vavr.
All images are courtesy of Aditya Bhargava, the author of the article above.
Functors
When a value is wrapped in a box, you can’t apply a normal function to it
This is where map comes in. map knows how to apply functions to values that are wrapped in a box.
Option.of(2).map(i -> i + 3); // Some(5)
Option.<Integer>of(null).map(i -> i + 5); // None
A functor is any type that defines how map works.
Here’s what is happening behind the scenes when we write Option.of(2).map(i -> i + 3)
Here’s what is happening behind the scenes when we try to map a function on an empty box
Now, what happens when you apply a function to a list?
List.of(2, 4, 6).map(i -> i + 3); // (5, 7, 9)
Functions are also functors!
In Java with vavr map
is called compose
or andThen
Function1<Integer, Integer> add3 = Function1.of(i -> i + 3);
Function1<Integer, Integer> add2 = Function1.of(i -> i + 2);
List.of(2, 4, 6).map(add3.compose(add2)); // (7, 9, 11)
Applicatives
Applicatives are like functors, except that not only the value is being wrapped in a context, but the function to apply to it also.
Monads
Monads apply a function that returns a wrapped value to a wrapped value
Suppose a function named half which only works on even numbers:
Option<Double> half(Double x) {
return x % 2 == 0 ? Option.of(x / 2) : Option.none();
}
But what if we feed it a wrapped value?
This is where bind also called flatMap or sometimes chain comes in!
Option.of(3d).flatMap(this::half); // None
Option.of(4d).flatMap(this::half); // Some(2)
If you pass in None
, it’s even simpler:
You can chain calls to flatMap:
Option.of(20d)
.flatMap(this::half) // Some(10)
.flatMap(this::half) // Some(5)
.flatMap(this::half); // None
So now we know that Option is a functor, an applicative and a monad.
Another example: user types a path, we load the file content and display it
Option<String> getLine() {
System.out.print("File: ");
return Option.of(new Scanner(System.in).next());
}
Option<String> readFile(String file) {
return Try.of(() -> new String(Files.readAllBytes(Paths.get(file)))).toOption();
}
Option<Boolean> putStrLn(String str) {
System.out.println("File content:\n");
System.out.println(str);
return Option.of(str.length() % 2 == 0);
}
And just call it like this:
getLine()
.flatMap(this::readFile)
.flatMap(this::putStrLn);
Some more code please!
How to FP in Java with Vavr
- Functions
- Composition
- Lifting
- Partial application
- Currying
- Memoization
- Values
- Option
- Try
- Either
- …
Functions
Java provides only:
Function
: accepts one parameterBiFunction
: accepts two parameters
Vavr goes up to 8 parameters:
Function0
,Function1
,Function2
, …- Support checked functions:
CheckedFunction1
,CheckedFunction2
, … - Support composition, lifting, currying and memoization
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
sum.apply(40, 2); // 42
Composition
Functions can be composed:
f : X -> Y
g : Y -Z
h : X -> Z = g(f(x))
Use compose
or andThen
for mor natural (for humans) order.
Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Function1<String, String> hi = s -> "Hi, " + s;
Function1<String, String> addMark = s -> s + " !";
Function1<String, String> welcome =
addMark.compose(hi.compose(upperCase));
welcome.apply("tars"); // Hi, TARS !
Function1<String, String> welcome2 =
upperCase.andThen(hi).andThen(addMark);
welcome2.apply("tars"); // Hi, TARS !
Lifting
Partial Functions are functions that are valid for a specific subset of values but may yield errors for some input
The lift
function lifts a partial function into a total function
- It can accept all input
- Returns an
Option
instead of the value - Instead of throwing an exception it will return
None
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lif(divide);
safeDivide.apply(15, 5); // Some(3)
safeDivide.apply(15, 0); // None
Partial application
Partial application allows you to create new function from an existing one by setting some arguments
It is not currying
Function2<Integer, Integer, Integer> multiply = (a, b) -> a * b;
Function1<Integer, Integer> twoTimes = multiply.apply(2);
multiply.apply(4, 3); // 12
twoTimes.apply(9); // 18
Function3<Long, Long, Long, Long> computation = (a, b, c) -> a + (b * c);
Function2<Long, Long, Long> incMultiply = computation.apply(1L);
incMultiply.apply(L2, 3L); // 1 + (2 * 3) = 7
Currying
Currying is the same as converting a function that takes n arguments into n functions taking a single argument each.
Function3<Long, Long, Long, Long> computation = (a, b, c) -> a + (b * c);
Function2<Long, Long, Long> incMultiply = computation.apply(1L);
incMultiply.apply(L2, 3L); // 1 + (2 * 3) = 7
Function1<Long, Function1<Long, Function1<Long, Long>>> curriedComputation =
computation.curried();
curriedComputation.apply(1L).apply(2L).apply(3L); // 1 + (2 * 3) = 7
Memoization
Memoization acts like some kind of caching, if you memoize a function, it will be only executed once for a specific input
Function0<String> cachedUUID = Function0.of(UUID::randomUUID)
.andThen(UUID::toString).memoized();
cachedUUID.apply(); // 88e93417-2adb-48f0-81f3-aa9bd41efad3
cachedUUID.apply(); // 88e93417-2adb-48f0-81f3-aa9bd41efad3
Function1<String, String> cachedUserUUID =
Function1.of((String user) -> user + ": " + UUID.randomUUID().toString())
.memoized();
cachedUserUUID.apply("Tars"); // Tars: d01562a7-1a13-45c0-b567-3239f4d0abc1
cachedUserUUID.apply("Kipp"); // Kipp: 304b1e2d-4c76-4785-9acc-de04ecf87730
cachedUserUUID.apply("Case"); // Case: 2e77448f-9a4d-4fc1-8b78-8805b035dd5b
cachedUserUUID.apply("Tars"); // Tars: d01562a7-1a13-45c0-b567-3239f4d0abc1
Values
Option
Option
is a monadic container with additions:
map
flatMap
filter
peek
- …
Represents an optional value: None
/ Some(value)
.
final String[] robots = new String[] {"Tars", "Kipp", "Case"};
Function0<String> randomRobot = () -> {
Random r = new Random();
boolean shouldFail = r.nextInt(10) > 5;
return shouldFail ? null : robots[r.nextInt(3)];
};
Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Option.of(randomRobot.apply()).map(upperCase); // None
Option.of(randomRobot.apply()).map(upperCase); // Some(KIPP)
Option.of(randomRobot.apply()).map(upperCase); // Some(TARS)
Option.of(randomRobot.apply()).map(upperCase); // Some(TARS)
Option.of(randomRobot.apply()).map(upperCase); // None
Option.of(randomRobot.apply()).map(upperCase); // Some(CASE)
Try
Try is a monadic container wich represents a computation that may either throw an exception of successfuly completes
Like Option
it has a lot of additions:
map
flatMap
filter
mapTry
peek
recover
onFailure
onSuccess
- …
final String[] robots = new String[] {"Tars", "Kipp", "Case"};
Function0<String> randomRobot = () -> {
Random r = new Random();
boolean shouldFail = r.nextInt(10) > 5;
if (shouldFail)
throw new RuntimeException("Plenty of slaves for my robot colony");
else
return robots[r.nextInt(3)];
};
Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Try.of(randomRobot::apply).map(upperCase); // Failure(Plenty of slaves for my robot colony)
Try.of(randomRobot::apply).map(upperCase); // Some(CASE)
Try.of(randomRobot::apply).map(upperCase); // Some(KIPP)
Try.of(randomRobot::apply).map(upperCase); // Failure(Plenty of slaves for my robot colony)
Try.of(randomRobot::apply).map(upperCase); // Failure(Plenty of slaves for my robot colony)
Try.of(randomRobot::apply).map(upperCase); // Some(CASE)
Try.of(randomRobot::apply).map(upperCase); // Some(TARS)
Either
Either represents a value of two types, it is either a Left
or a Right
.
By convention Left
is the failure case, and Right
the success case, like Option
and Try
it has a lot of additions:
right
left
map
flatMap
filter
- …
Function0<Either<Throwable, String>> safeRandomRobot =
() -> {
Random r = new Random();
boolean shouldFail = r.nextInt(10) > 5;
return shouldFail ? Either.left(new RuntimeException("Plenty of slaves for my robot colony"))
: Either.right(robots[r.nextInt(3)]);
};
// Plenty of slaves for my robot colony
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// TARS
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// TARS
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// CASE
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// Plenty of slaves for my robot colony
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
// KIPP
safeRandomRobot.apply().map(upperCase).getOrElsGet(Throwable::getMessage);
That’s all for now, I encourage you to try Vavr, it can make your code both cleaner and safer at the same time.
Edit: See also my other article about how to use Try
efficiently in the context of a pipeline.
Cheers!