The monadic pattern

Posted on by Mandla Mbuli

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.

  1. You need a constructor, you need a way to get things into the container/holder.

      def of[B](a: A): M[B] =
        M(a)
  2. you need map, which is kinda like saying instead of me giving you access to my internal state a through things like getA() or setA(), you tell me what you want to do with a when you have it and I will do it for you. So map is a method that takes a function that accepts a 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 DirtyApple A and I ask you to cleanIt mapper so I have you holding a CleanApple B.

    The map method can now look like:

    public CleanApple cleanIt(DirtyApple dirtyApple);
    public <CleanApple> FoodHolder<CleanApple> map(Function<DirtyApple, CleanApple> mapper);

    and used like:

     foodholder.map(dirtyApple -> cleanIt(dirtyApple))
  3. 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<FoodHolder<CleanApple>> travellingFoodHolder = houseFoodholder.map(apple -> handover(apple)); 

    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:

     FoodHolder<CleanApple> travellingFoodholder = houseFoodholder.flatMap(apple -> handover(apple));

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 =
    data.isDefined

  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 =
    data.get

  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(
      successHandler: Data => Unit,
      failureHandler: List[Error] => Unit
  ): 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(
      r2: Result[Data],
      combiner: (Data, Data) => Data
  ): Result[Data] =
    return flatMap(d1 => r2.map(d2 => combiner(d1, d2)))

end Result // I like this

// 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