Overfullstack Overfullstack
Functional Programming

Monads for Drunken Coders, Pint-1

A chilled introduction to the Dreaded Monad, using Java 8

Story of an Egg validator

Intro

Sol 1: One egg - One validation

Life is so simple. Pass that one egg through that one validator. Results in good or bad.

Sol 23: Many eggs - One validation

Not difficult at all, simply pass them through validator, one after the other and collect the results for each one, in order. With simple if-else condition, this code looks like a Cute Sprout! 🌱

Sol 97: Many eggs - Many validations

Why do I sense climate’s getting a bit hotter. Ok, still no problem, I know Java 8. Let me write a pipe of filter functions. Each of them just pass the good ones ahead and discard bad ones.

firstfunctionalcode.java
eggs.stream()
.filter(EggValidator::validator1)
.filter(EggValidator::validator2)
.filter(EggValidator::validator3)
....
...
..

Yay! I’m a Functional programmer! Let me have a šŸŗ

Suddenly, the cute sprout turned into a treešŸŽ‹, with multiple if-else-break-continue branches of execution.

Sol 179: Many Types of eggs - Many validations

Sol 237: Many Types of eggs - Many more validations - in parallel

I think, I’m too drunk. My head is spinning! 🤯

This design pattern has a name and it’s called the ā€œEvolution-of-a-Problem-Over-Timeā€.

The code ended-up like an Alien plant šŸ‘½

cyclomaticeggvalidator.java
void cyclomaticCode() {
var eggList = Egg.getEggCarton();
Map<Integer, ValidationFailure> badEggFailureBucketMap = new HashMap<>();
var eggIndex = 0;
for (var iterator = eggList.iterator(); iterator.hasNext(); eggIndex++) {
var eggTobeValidated = iterator.next();
if (!Operations.simpleOperation1(eggTobeValidated)) {
iterator.remove(); // Mutation
// How can you cleanly map validation-failure to which validation-method failed?
badEggFailureBucketMap.put(eggIndex, VALIDATION_FAILURE_1);
continue;
}
try {
if (!Operations.throwableOperation2(eggTobeValidated)) {
iterator.remove();
badEggFailureBucketMap.put(eggIndex, VALIDATION_FAILURE_2);
}
} catch (Exception e) { // Repetition of same logic for exception handling
iterator.remove();
badEggFailureBucketMap.put(eggIndex, ValidationFailure.withErrorMessage(e.getMessage()));
continue;
}
try { // Inter-dependent validations
if (Operations.throwableOperation31(eggTobeValidated)) {
var yellowTobeValidated = eggTobeValidated.getYolk();
if (yellowTobeValidated != null) { // Nested-if for null checking nested objects
try {
if (!Operations.throwableAndNestedOperation32(yellowTobeValidated)) {
iterator.remove();
badEggFailureBucketMap.put(eggIndex, VALIDATION_FAILURE_32);
}
} catch (Exception e) {
iterator.remove();
badEggFailureBucketMap.put(eggIndex, ValidationFailure.withErrorMessage(e.getMessage()));
}
}
} else {
iterator.remove();
badEggFailureBucketMap.put(eggIndex, VALIDATION_FAILURE_2);
}
} catch (Exception e) {
iterator.remove();
badEggFailureBucketMap.put(eggIndex, ValidationFailure.withErrorMessage(e.getMessage()));
}
}
for (var entry : badEggFailureBucketMap.entrySet()) {
System.out.println(entry);
}
}

Imperative vs Functional Chatter

Problem.split()

But rather than solving them one-by-one, it’s important to find a paradigm, which can solve these problems as a group.

Octopus Functions

Imperative Responsibility

imperativelastname.java
public static String concatLastNames(List<String> team) {
var output = new StringBuilder();
var isFirst = true;
for (var teamMemberName : team) { // Concern-1: Looping through the list
if (teamMemberName != null) { // Catch-1: Deal with nulls
teamMemberName = teamMemberName.trim(); // Catch-2: Deal with only white space names
if (!teamMemberName.isEmpty()) { // Catch-3: Deal with empty names
if (!isFirst) { // Catch-4: Should not prepend delimiter for first entry.
output.append(DELIMITER);
}
var lastName = extractLastName(teamMemberName); // Concern-2: Extracting last name
output.append(lastName); // Concern-3: Aggregating the results with the delimiter.
isFirst = false;
}
}
}
return output.toString();
}
private static String extractLastName(String fullName) {
return fullName.substring(fullName.lastIndexOf(" ") + 1);
}

Behead the Octopus, Lego the Focussed Functions

functionallastname.java
private static final UnaryOperator<String> GET_LAST_NAME =
fullName -> fullName.substring(fullName.lastIndexOf(" ") + 1);
void lastNameCollectorWithStream() {
final var expected = TEAM.stream()
.filter(Objects::nonNull) // Catch-11: Deal with nulls.
.map(String::trim) // Catch-12: Deal with only white space strings.
.filter(not(String::isEmpty)) // Catch-13: Deal with empty strings.
.map(GET_LAST_NAME)
.collect(Collectors.joining(DELIMITER));
}

Flow Heterogeneous data Fluently

pseudovalidator.java
Stream<Egg> validatedEggStream = eggs.stream().map(egg -> validate(egg));
private <what-should-I-return?> validate(Egg egg) {
boolean isValid = false;
if (!egg.isRotten()) {
if (egg.getYellow() != null) {
try {
makeHalfBoiledOmelette(egg); // My Fav Omlet
isValid = true;
} catch (EggException e) {
return <How-to-return-exception?>; // case 1 exception
}
} else if (egg.getEggWhite() != null) {
eggWhiteDefect = examineEggWhite(egg); // case 2 inter-dependent validation fails
isValid = (eggWhiteDefect == null);
} else {
return <not-an-egg>; // case 3
}
}
return isValid ? egg : <How-to-return-defect?>; //case 4
}

Let’s take a fork here and visit the Monad-Land to understand Containerization.

Functors

functor.java
public class Functor<T> {
private final T value;
public Functor(T value) {
this.value = value;
}
public <U> Functor<U> map(Function<T, U> mapperFunction) {
return new Functor<>(mapperFunction.apply(value)); // Functor wrapping f(x)
}
}

The Siblings - map(), flatMap()

minions

The Dawn of the Monad

The curse of the monad is that once you get the epiphany, once you understand - ā€œoh that’s what it isā€ - you lose the ability to explain it to anybody. - Douglas Crockford

Monads are Functors, which also implement flatmap and abide by some Monad laws.

monad.java
public class Monad<T> {
private final T value;
public Monad(T value) {
this.value = value;
}
public <U> Monad<U> map(Function<T, U> mapperFunction) {
return new Monad<>(mapping.apply(value));
}
public <U> Monad<U> flatMap(Function<T, Monad<U>> mapperFunction) {
return mapperFunction.apply(value);
}
}

Enough of Theory! how can this help the problem at hand?

Problems.split().stream()

.map(this::solve)

Post credits scene: Making of a Monad

Now you see it? Now you don’t? now-you-see-me

Wanna see how the entire pipeline works seamlessly with the Monad, even with some exceptional eggs blown in-between? The sequel brings-in some new names like Immutability, Parallelism, Memoization and X-Men evolution (Just kidding!)

Let’s cook a Monad.

Well, I couldn’t find time to prepare a pint-2, but this talk I gave covers everything about what is discussed till now and further:

Back to all articles