The monadic pattern
Prelude
I’ve noticed this pattern a lot. It appears in multiple places. I think it common enough now that I can even write about it. I am talking about the monadic pattern not the genesis of powershell. I just want to present the pattern and talk about why I find it useful.
I don’t really want to go into Haskell, so I will stick to Scala and Java with the exception of Promises.
What is it?
It is basically classes that contain or hold things that also have methods map
and flatMap
.
You need a constructor, you need a way to get things into the container/holder.
def of[B](a: A): M[B] = M(a)
you need
map
, which is kinda like saying instead of me giving you access to my internal statea
through things likegetA()
orsetA()
, you tell me what you want to do witha
when you have it and I will do it for you. Somap
is a method that takes a function that acceptsa
and does something with it.in Scala this is
def map[B](mapper: (A) ⇒ B): SomeClass[B]
in Java this can be something like
// mapper is a function that takes something of type A and returns // something of type B public <B> SomeClass<B> map(Function<A, B> mapper);
A ridiculous example is: you’re my foodholder
SomeClass
. You’re currently holding my DirtyAppleA
and I ask you to cleanItmapper
so I have you holding a CleanAppleB
.The map method can now look like:
public CleanApple cleanIt(DirtyApple dirtyApple); public <CleanApple> FoodHolder<CleanApple> map(Function<DirtyApple, CleanApple> mapper);
and used like:
.map(dirtyApple -> cleanIt(dirtyApple)) foodholder
you need a
flatMap
I am about to travel and I need you to handover the food to my travelling FoodHolder
public FoodHolder<CleanApple> handover(CleanApple apple);
if I used map in this situation I would get:
<FoodHolder<CleanApple>> travellingFoodHolder = houseFoodholder.map(apple -> handover(apple)); FoodHolder
This is weird, my travelling foodholder is holding my house foodholder? So I “flatten” the food holders (tell the house foodholder to stay home) then I can have just my travelling foodholder holding food.
flatMap is for situations like you’re my FoodHolder and I ask you to perform an action that leads to you holding another FoodHolder. I ask you to map but the mapper I give you returns another FoodHolder.
trait M[T] { def flatMap[U](mapper: T => M[U]): M[U] }
in Java
// mapper is a function that takes something of type A and returns // something of type B public <B> SomeClass<B> flatMap(Function<A, SomeClass<B>> mapper);
This would like this with FoodHolder
public FoodHolder<CleanApple> handover(CleanApple apple); public <CleanApple> FoodHolder<CleanApple> flatMap(Function<DirtyApple, FoodHolder<CleanApple>> mapper);
used like:
<CleanApple> travellingFoodholder = houseFoodholder.flatMap(apple -> handover(apple)); FoodHolder
Stream
The Java Stream API is basically a way to provide the monadic pattern for Java Collections. It exists just to that Java collections can have map and flatMap.
Example
Find the length of the longest word
var listFalse = Stream.of("analyze", "llitre", "colour")
.mapToInt(word -> word.size())
.max()
Why would the Java maintainers create a whole API just for flatMap and map
Cause they’re just that useful! A lot of the intermediate operations (maybe even all of them) can be expressed using the monadic pattern.
e.g. filter, tap, mapToInt, peek
I consider it an evolution
for (int i = 0, i < 10, i++) for (int i : List.of(1, 2, 3, 4, 5, 6)) List.of().forEach() List.of().stream().filter().map()
Reduce is out of scope
Promise
Promises have 3 states: Pending, Fulfilled Resolved, Broken Rejected. The Fox says
I will go to the mountains to look for the sworn (PENDING) you tell me what to do if I find it (RESOLVED) or if I can’t find it (REJECTED)
- does it have map?
- kinda similar to CompletableFuture, but turns out most Java devs I encounter have not done frontend, this was not obvious to me.
CompletableFuture
- concept
- the names are different
- exceptions
Optional
- concept
- spoke about it last year
- leads to Result
Result
- concept
- something I’ve been playing with recently
- my implementation
- errors as values (a declarative style)
- async code
- more limited than Either (let’s use Rust Result instead of Either)
- ideas from Optional
- .isSuccess
- .isFailure
- .get
- .getOrThrow(ErrorContext)
- ideas from Stream
- .tap
- .tapErrors
Scala3 naive implementation or Result
package me.mandla
package result
import java.{util => ju}
// 1. Immutable internal state
// private constructor because I want my constructors to be used to reduce chance of
// internal scope inconsistency:
// if there are errors, data should be None, if there is Data, errors should be empty
class Result[Data] private (data: Option[Data], errors: List[Error]):
// 3. ability to use and even "change" the immutable internal state
def map[NewData](mapper: Data => NewData): Result[NewData] =
// makes assumption about the internal consistency I touched on in the private constructor.
if (data.isDefined)
// allows manipulation of the data
Result.success(mapper(data.get))
else
// propagates the error if there is one
Result.failure(errors)
// 4. Optional inspired .or()
// Usage of .or()
// cache.get(key)
// .or(service.get(key))
// .map(item -> item.name)
// The implementation does the map code again, cause there is a problem with supplier returning another Result
def or(supplier: () => Result[Data]): Result[Data] =
// The implmentation when you don't have flatMap
// if (data.isDefined)
// Result.success(data.get)
// else
// supplier()
// 6. Refactor or to use flatMap
flatMap(_ => supplier())
// 5. flatMap can make writing or a little cleaner,
// but I am not smart enough to stay in the Result context in order to implement it, so 4 more methods are added
def flatMap[NewData](mapper: Data => Result[NewData]): Result[NewData] =
if (data.isDefined)
// handle the mapper result
val res = mapper(data.get)
if (res.isSuccess())
Result.success(res.get()) // why does this need to be called with ()?
else
Result.failure(res.getErrors)
else
// propergates the error if there is one
Result.failure(errors)
def isSuccess(): Boolean =
.isDefined
data
def isFailure(): Boolean =
!errors.isEmpty
// Inspired by Optional.get, an escape hatch basically which I guess should be avoided
// This is the same as orElseThrow
def get(): Data =
.get
data
def getErrors: List[Error] =
errors
// 7. I wanted to log the data while I was deciding what to do with it (actually either)
def tap(tapper: Data => Unit): Result[Data] =
map(d => {
tapper(d)
d})
// 8. I wanted to log the errors while I how to properly handle them (actually either)
// But it needs a map for errors, se
def tapErrors(tapper: List[Error] => Unit): Result[Data] =
handle(errors => {
tapper(errors)
errors})
def handle(mapper: List[Error] => List[Error]): Result[Data] =
// makes assumption about the internal consistency I touched on in the private constructor.
if (data.isDefined)
// propagates the success if there is one
Result.success(data.get)
else
// allows manipulation of the errors
Result.failure(mapper(errors))
// 9. flatMap for errors
def catchAll(mapper: List[Error] => Result[Data]): Result[Data] =
if (data.isDefined)
// propergates the success if there is one
Result.success(data.get)
else
val res = mapper(errors)
if (res.isSuccess()) // you can swallow errors if you want
Result.success(res.get())
else
Result.failure(res.getErrors)
// Either has this why not
def either(
: Data => Unit,
successHandler: List[Error] => Unit
failureHandler): Unit =
tap(successHandler)
.tapErrors(failureHandler)
def filter(predicate: Data => Boolean): Result[Data] =
flatMap(d => {
val keep: Boolean = predicate(d)
if (keep) {
Result.success(d)
}
// I have fixed ErrorCodes
Result.failure(result.Error("NOT_FOUND", "filtered out" + d.toString()))
})
// Just because Future has a similar function
def combine(
: Result[Data],
r2: (Data, Data) => Data
combiner): Result[Data] =
return flatMap(d1 => r2.map(d2 => combiner(d1, d2)))
Result // I like this
end
// In order to define the static constructors, I need the companion object.
// Don't know if it is possible to have the constructors without this.
object Result:
// 2. you need constructors for the internal state
// - It is not using Data, this would be a problem for map
def success[SomeType](data: SomeType): Result[SomeType] =
Result(Some(data), List())
def failure[SomeType](errors: List[Error]): Result[SomeType] =
Result(None, errors)
def failure[SomeType](error: Error): Result[SomeType] =
Result(None, List(error))
class Error(val code: String, val description: String)
// Notes:
// I'm writing out the types by name as an experiment for myself for type level programming, you're meant to read the types
// in Scala (maybe even Java 17) this would a sum type
// Scala types come after, this is... different from Java
// def is for defining functions
// object is basically a singleton that I'm using as a namespace cause of the Kingdom of nouns
//
// All the functions are implemented using map/handle or flatMap/catchAll
// Now need to tell the story of how each of them are used in code