(Examples can be found on GitHub)
We have decoupled data from its usage by replacing more complicated control flow ideas (like loops null and throw) with combinators by passing functions as parameters. This allows us to think differently about how we write programs.
Instead of thinking about software as objects acting on themselves; we can start to think of it as two parts:
- Data
- Functions which define Data flow
To illustrate, we'll redefine our color function as data.
sealed trait Fish{ val name: String val color: String } case object OneFish extends Fish{ override final val name: String = "One Fish" override final val color: String = "Red Fish" } case object TwoFish extends Fish{ override final val name: String = "Two Fish" override final val color: String = "Blue Fish" } case object NotFish extends Fish{ override final val name: String = "Ahab" override final val color: String = "White Whale" }
Here, we model our fish data in three points; two good states and a bad state. Instead of modeling a two point set of data with Strings, we model it as three discrete states using case classes.
Case Classes are automatically encapsulated and immutable by default and, as the states are encoded into the types, there is no need to use null or throw.
At this point, we have built up a nice base for functional programming. We can define robust data structures which model our problem space very closely and functional constructs which allow us to transform that data. This brings us to the salient feature of this process: Objects are not Coroutines.