In the previous posts, we went over how to introduce immutability, combinators and case classes to move toward functional programming. These three points together are the basis for the point described in this point that Objects are not Coroutines.
If you are unfamiliar with coroutines, wikipedia has a basic description of them.
In Java, the usual application runs a little like this:
- Initialize an object
- Perform an operation
- Mutate the object
- Perform an Operation
- ...
This habit breaks all of the FP ideas we have developed so far.
When introducing Typelevel Scala, it is important to note we are not simply adding a library to an already existing system (the JVM). We are trying to change how people do their day to day work. Some of them have been writing OO Imperative software products for decades making a change of paradigm difficult. Keeping to simple language is key to our goal of shifting an organization's workflow.
The workflow shift we are suggesting here is:
- Define
- Apply Combinator
- ...
We will be moving from a paradigm based in mutability and state to one built on immutability and functions.
OO Imperative Style
We will be defining a school of fish and how that school of fish grows.
class BadSchool(){ private var name: String = null private var depth: Depth = null private var location: Location = null private var fish: mutable.Buffer[Fish] = null def setName(newName: String): Unit = { name = newName } def getName(): String = name def setDepth(newDepth: Depth): Unit = { depth = newDepth } def getDepth(): Depth = depth def setLocation(newLocation: Location): Unit = { location = newLocation } def getLocation(): Location = location def setFish(newFish: mutable.Buffer[Fish]): Unit = { fish = newFish } def removeFish(aFish: Fish): Unit = { fish -= aFish } def addFish(aFish: Fish): Unit = { fish += aFish } def getFish(): mutable.Buffer[Fish] = fish override def toString(): String= { s"School(\n\t$name,\n\t$depth,\n\t$location,\n\t$fish)" } }
In my experience, this is the type of code commonly written by people who have just made the jump from an OO language into Scala. Here we initialize the object's members to null and use mutable containers to maintain and augment state. A typical use case would look like:
def asCoroutine(): Unit = { val coroutine = new BadSchool() val (name, depth, location, fish) = someInit() coroutine.setName(name) coroutine.setDepth(depth) coroutine.setLocation(location) coroutine.setFish(fish) convertToJsonAndPutOnTheWire(coroutine) var newFish: Fish = null for(i <- (0 to 10)){ newFish = nextFish(coroutine) coroutine.addFish(newFish) convertToJsonAndPutOnTheWire(coroutine) } }
This application is difficult to follow and very cluttered. In order to create a new valid school of fish, one must initialize the object and call four set methods. When growing a school of fish, the developer needs to destroy the previous school of fish forcing any interaction with the object to be synchronous.
This is like a very messy coroutine. The addFish method is like a yield and there is no way to get back to the previously returned yield state.
Typelevel Style
The above impure code can be fairly simply converted to a more FP style. First we define our school of fish.
case class School( name: String, depth: Depth, location: Location, fish: immutable.Queue[Fish])
This is short and to the point. The intent of the code is clear and there are no messy methods defined for maintaining, getting and mutating state. The same use case would be implemented functionally like:
def aBetterWay(): Unit = { @annotation.tailrec def perform(qty: Int, acc: List[School]): List[School] = { if(qty > 0 && acc.nonEmpty){ val head :: tail = acc val currentFish = head.fish.last val next = nextFish(currentFish) val result = head.copy(fish = head.fish.enqueue(next)) perform(qty - 1, result :: acc) }else acc } val school = School( "Bikini Bottom", Deep, South, immutable.Queue(OneFish)) val result = perform(10, List(school)) result.foreach(convertToJsonAndPutOnTheWire) }
There are two main ideas:
- In lieu of initializing state we define data.
- Once data is defined, it cannot be redefined.
The function buildSchool builds a new school of fish from a provided school of fish. State is created, never destroyed, and there is no initialization step. Moreover, the webservice call can be made asynchronous without worry for synchronization or heap issues.
Next, we'll introduce our first libraries: Monocle and Argonaut.