Algebraic Types in Scala???
If you have ever looked into Scala at any point you would’ve heard the term Algebraic Data Types
The goal here is to attempt to explain what that is since it’s kind of an obscure concept from an OO view point. Even though you could draw a parallel between algebraic types and Composition vs Inheritance, it’s not quite the same thing. So it’s worth while to try to understand ADT for what it is.
Disclaimer, I didn’t come up with all this material on my own, I did use a number of references which you can see at the bottom because credit should be given where credit is due (check out the references). Oh and before I forget, I’m sure there are a ton of spelling mistakes here, so I apologize in advance.
All this Scala stuff is just so exciting no time to think about spelling ¯\_(ツ)_/¯
What do you mean by “algebraic?”
Let’s do some counting.
How many values of type Nothing? → 0
How many values of type Unit? → 1
How many values of type Boolean? → 2
How many values of type Byte? → 256
How many values of type String? → many
How many of type (Byte, Boolean)? → 2 × 256 = 512
How many of type (Byte, Unit)? → 256 × 1 = 256
How many of type (Byte, Byte)? → 256 × 256 = 65536
How many of type (Byte, Boolean, Boolean)? → 256 × 2 × 2 = 1024
How many of type (Boolean, String, Nothing)? → 2 × many × 0 = 0
To figure out the cardinality of a tuple type like this, you multiply the cardinalities of the component types.
Product types! This and That
Tuples!
type Person = (String, Int)
Classes!
class ScalaPerson(val name: String, val age: Int) class Person { final String name; final Int age; ... }
- Pretty much every language has product types like tuples or records or structs or classes.
- So we all have an instinct about multiplying types. But you can also add them, which isn’t something most of us think about.
Let’s do some more counting.
So this is something you may not ever think about if you’re coming from a traditional object oriented language. What if you have a choice of one thing or another?
How many of type Byte or Boolean? → 2 + 256 = 258
How many of type Boolean or Unit? → 2 + 1 = 3
How many of type (Byte, Boolean) or Boolean? → (256 × 2) + 2 = 514
How many of type Boolean or (String, Nothing)? → 2 + (many × 0) = 2
Sum types! This or That
Nothing = 0 Unit= 1 Boolean = 2 Byte= 256 (Unit , Boolean) = 1 × 2 = 2 Unit | Boolean= 1 + 2 = 3 // not Scala syntax (Byte , Boolean) = 256 × 2 = 512 Byte | Boolean= 256 + 2 = 258 (Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean) = 256 // sums all the way down Boolean = true | false Int = 1 | 2 | 3 | ...
- So we’ll get to the exact encoding of this idea in Scala, but I wanted to explain this notion of multiplication and addition of types because that’s where the “algebraic” comes from.
- And we can do much more than add and multiply; function types are actually exponentials, and the derivative of a type is a structure called a one-hole context that’s the key to a family of data structures called zippers.
- So something interesting we see is that a tuple of 8 booleans has exactly the same number of inhabitants as byte. So the types are isomorphic, you can line them up 1:1 and convert between them without losing information.
- Another interesting thing is that anything times unit is just that thing; unit is the multiplicative identity for types. The takeaway is that there is never any reason to use unit in a product type.
- Understanding when types are equivalent is a nice skill to have.
Sum types in Scala
Encoded by Subclassing
sealed trait Pet case class Cat(name: String) extends Pet case class Fish(name: String, color: Color) extends Pet case class Squid(name: String, age: Int) extends Pet val bob: Pet = Cat("Bob")
In Scala when you hear ADT it means sum type.
Ok so we have a sealed trait, which means all implementations of that trait must be in the same source file so the compiler knows about all of them. And I’ll explain why that’s important bellow.
We don’t think of this as a class hierarchy. We think of it as a single type, Pet, with three constructors.
So Pet is a sum of three products. And if bob is a pet, he can be a cat or a fish or a squid, which we inspect…
De-structured by pattern matching
def sayHi(p: Pet): String = p match { case Cat(n)=> "Meow " + n + "!" case Fish(n, _)=> "Hello fishy " + n + "." case Squid(n, _) => "Hi " + n + "." } scala> sayHi(Cat("Bob")) res0: String = Meow Bob! scala> sayHi(Squid("Steve", 10)) res1: String = Hi Steve.
We provide a match with a pattern for each constructor, and the arguments we used to construct the value are bound to variables when we do this so they’re available on the right-hand side. So that’s a different n in each case.
The underscore means we don’t care about the value.
Exhaustiveness Checking
def sayHi(p: Pet): String = p match { case Cat(n)=> "Meow " + n + "!" case Fish(n, _)=> "Hello fishy " + n + "." } <console>:14: warning: match may not be exhaustive. It would fail on the following input: Squid(_, _) p match { ^
We get a compiler warning. So this is exhaustive matching, which you get if you use a sealed trait because the compiler knows that there are exactly three constructors for Pet.
So you don’t always need to use pattern matching because there might be methods on the parent trait that do what you want, but you can always fall back on it if you’re not sure what to do.
There is another more general way we can encode de-structuring, called a fold. We will see this in a minute.
Standard Library: Option
Computations that may fail to return a value
sealed trait Option[+A] case object None extends Option[Nothing] case classSome[A](a: A) extends Option[A]
Example
def safeDiv(a: Int, b: Int): Option[Int] = if (b != 0) Some(a / b) else None scala> val x = safeDiv(10, 2) x: Option[Int] = Some(5) scala> val y = safeDiv(10, 0) y: Option[Int] = None
This is what we do instead of using null in Scala
Standard Library: Either
Computations that may return this or that
sealed trait Either[+A, +B] case class Left[A](a: A)extends Either[A, Nothing] case class Right[B](b: B) extends Either[Nothing, B]
Example
def safeDiv(a: Int, b: Int): Either[String, Int] = if (b != 0) Right(a / b) else Left("Divide by zero!") scala> val x = safeDiv(10, 2) x: Either[String,Int] = Right(5) scala> val x = safeDiv(10, 0) x: Either[String,Int] = Left(Divide by zero!)
- You can use this to return this or that; most commonly you see left for errors and right for success.
Standard Library: Try
Computations that may fail with an exception
sealed trait Try[+A] case class Success[A](a: A) extends Try[A] case class Failure[A](t: Throwable) extends Try[A]
Example
import util._ // it's scala.util.Try def safeDiv(a: Int, b: Int): Try[Int] = Try(a / b) scala> val x = safeDiv(10, 2) x: scala.util.Try[Int] = Success(5) scala> val x = safeDiv(10, 0) x: scala.util.Try[Int] = Failure(java.lang.ArithmeticException: / by zero)
Standard Library: List
Computations that may return many answers
sealed trait List[+A] case object Nil extends List[Nothing] case class ::[A](head: A, tail: List[A]) extends List[A] object List { def apply[A](as: A*): List[A] = ... }
Example
def sum(ns: List[Int]): Int = ns match { case Nil => 0 case n :: ns => n + sum(ns) } scala> sum(List(1,2,3)) res23: Int = 6