What a delightful idea to come back from vacation with batteries fully charged and with some wacky ideas around our minds to write about. Best of these came from the very influence of joints the moon.
ADT?
An Algebraic Data Type (TDA from now so we can save money for each word in WordPress) is just a way to express a data type (Cat, Dog, Prevarication) based on an algebra. And when we say ‘algebra’, we mean type sums and products (of Integers, Cats, Cars, Prevarications, …). For example:
Train = Locomotive + Wagon * Train
How do one read that? A train may be: a locomotive OR a wagon AND another train (that may be as well a wagon and another train, that may be as well a …).
Take a look at both disjunction and conjunction: the sum represents an OR, and the product represents an AND (like Boole algebra).
It’s also worthy to notice that, from this type definition you can infer a recursive pattern. With the Train type, the base case is definitively the Locomotive and, at the recursive case, we have a wagon and another train. As we’ll see later, this pattern is very frequent and makes easier the type definition.
And how are sum and product represented in Scala?
The easier way to represent the type sum (also called co-product), in a paradigm with polimorphism support (in general) and in Scala (in particular), is just the inheritance feature. If we have the following case:
sealed trait Animal case object Cat extends Animal case object Dog extends Animal
we’re indeed expressing a type co-product:
Animal = Cat + Dog
that is, an Animal
can only be, a Cat
, or a Dog
.
Regarding the product, we could define it as the attribute set that compounds a certain type instance. For example,
case class Student(name: String, age: Int)
expressed as a product sum, would be as follows:
Student = String * Int
So, for building a Student
instance, you need a String
and an Int
.
If we try now to materialize the previously exposed train model (with some additives) we’ll notice that
Wagon = String * Int Train = Locomotive + Wagon * Train
is translated into Scala as
sealed trait Train case object Locomotive extends Train case class Wagon(model: String, passengers: Int) case class Nexus(wagon: Wagon, next: Train)
So what is it good for?
…absolutely nothing, listen to me♩♪♫♬.
If you think, my fellow, that this is stuff that nobody uses, you haven’t thought about which scala.Prefef
structures are defined this way. List
s, for example, as defined as:
trait List[+T] case object Nil extends List[Nothing] case class ::[T](head: T, tail: List[T]) extends List[T]
That is, a List can be, an empty one, or an element followed by another list.
If we express that idea in terms of products and co-products:
List[T] = EmptyList[T] + NonEmptyList[T] NonEmptyList[T] = T * List[T]
Please, notice that, the case of the empt list (Nil
) has a bizarre but beautiful implementation in Scala.
If we try to define an empty list for eeeeeeeeeevery single existing type, we would have to instantiate a Nil[Cat]
, a Nil[Dog]
, …
In order to avoid this, and having an only Nil
, we make it extend from List[Nothing]
that, as you’ll probably remember from other posts, Nothing
extends from eeeeeeeeevery single existing type (both primitive and programmer defined). If we add the fact of List[T]
being covariant at T
, we’ll have an only object Nil
that represents the empty lists for all types. Awesome, right?
Example: Even numbers
In order to harden to this new way of thinking, let’s suppose the following challenge: how could we represent even numbers in Scala?
Requirements
If we’re not sophisticated enough and we trust a lil’ bit in runtime assertions, we could say that even numbers are defined as:
case class Even(value: Int) { require(value%2==0, "it's not even") }
But, if we try to create an Even
with an odd integer number we’ll get a giant NOPE:
Even(1) java.lang.IllegalArgumentException: requirement failed: it's not even at scala.Predef$.require(Predef.scala:233) at Even.<init>(<console>:7)
However this assertion won’t be verified until run-time, the moment when require
is executed. Thus, our code could be compiled without being correct…
We can do it much better…
Next(Next(…))
Another option is to assume (and we won’t discuss about it) that zero is an even number, that we have infinite memory installed in our machine, that the overflow error doesn’t exist…
In that, not so far, case (…), we could define even numbers as:
sealed abstract class Even(val value: Int) case object Zero extends Even(0) case class Next(previousEven: Even) extends Even(2 + previousEven.value)
So, if we have a method that generate reservations for the Boat Love, that requires an even number of participants, we can use our brand new defined Even
type:
def loveBoatReservation( peopleAmount: Even): Reservation = ???
Given there’s no way to build an Even
from an integer that is not even, we avoid uncomfortable situations at runtime, where the amount of people that get on the Love Boat are odd. Otherwise, someone could be…
Recursive ADTs and its techniques
Once the data type is defined, let’s suppose we want to implement the sum of even numbers:
def sum(e1: Even, e2: Even): Even = ???
We handle several alternatives. One of them could be the quick-and-dirty one:
def sum(e1: Even, e2: Even): Event = new Even(e1.value + e2.value){}
But take a closer look at the fact that we’re totally ignoring the constructors we’ve defined. If we want to use pattern matching over the result:
val four = new Even(4){} sum(Zero, four) match { case Zero => //it never gets inside this case! case Next(Next(Zero)) => //OMG! IT DOESN'T FIT HERE EITHER! } scala.MatchError: $anon$1@649f2009 (of class $anon$1)
The other technique (much more sophisticated by the way) consists on invoking a recursive method that, for each call, it decreases the second even number, while it increases the first one. For doing so, we make use of Next
apply(constructor) and unapply(extractor) methods:
def sum(e1: Even, e2: Even): Even = { @tailrec def rSum(ev1: Even, ev2: Even): (Even, Even) = { ev2 match { case Zero => (ev1, Zero) case Next(e) => rSum(Next(ev1), e) } } val (result, _) = rSum(e1, e2) result }
Undeniably beautiful 🙂
Conclusions
Apart from becoming a lil’ bit crazier when reading back-from-vacation posts, we can extract several main conclusions from what we’ve read:
- As we always say, every possible assertion that we can check at compile time instead of runtime, it’s saving time and headaches hunting bugs of software in production (which is more expensive and more keen to make heads roll).
- Constructors are THE key. If we define an ADT, we can control that generated values of that type are correct by defining the proper constructors:
Zero
andNext
. In both cases, we are certainly sure that even number rules are satisfied. - Methods that operate over recursive data types use to be recursive as well. And, apart from that, for generating values of the mentioned type (
Even
in our example) they should only use the existing constructor methods.
In a future post, we’ll talk about the relation between data types algebra and definition of formal grammars…or not.
Peace out!
[…] may (or may not) remember that we occasionally mentioned co-products when we spoke about algebraic data types. We defined them as the natural way of expressing type union(Wine = White U Red U Espumoso). In […]
Me gustaMe gusta