Algebraic Data Types and Origami Patterns

Preview:

Citation preview

ORIGAMI patterns with Algebraic Data Types

@remeniuk

What is “algebra”?

1. A set of elements 2.Some operations that map

elements to elements

List

1. Elements: “list” and “list element”

2.Operations: Nil and ::(cons)

In programming languages, algebraic data types are defined with the set of constructors that wrap other types

Haskell offers a very expressive syntax for defining Algebraic Data Types.

data Boolean = True | False

Here's how Boolean can be implemented:

data Boolean = True | False

This is a closed data type - once you've declared the constructors, you no longer can add more dynamically

True and False are data type constructors

In Scala, algebraic data type declaration

is a bit more verbose...

sealed trait Boolean case object True extends Boolean case object False extends Boolean

*sealed closes the data type!

Scala vs Haskell

Simple extensibility via inheritence - open data types

Syntactic clarity >

Regular algebraic data types

• Unit type

• Sum type: data Boolean = True | False

• Singleton type : data X a = X a

Combination of sum and singleton : Either a b = Left a | Right b

• Product type: data List a = Nil|a :: List a

(combination of unit, sum and product)

• Recursive type

data ListI = NilI | ConsI Integer ListI

List of Integers in Haskell:

data ListI = NilI | ConsI Integer ListI

Usage:

let list = ConsI 3 (ConsI 2 (ConsI 1 NilI))

Not much more complex in Scala...

trait ListI case object NilI extends ListI case class ConsI(head: Int, tail: ListI) extends ListI

...especially, with some convenience methods...

trait ListI { def ::(value: Int) = ConsI(value, this) } case object NilI extends ListI case class ConsI(value: Int, list: ListI) extends ListI

...and here we are:

val list = 3 :: 2 :: 1 :: NilI

Lets make our data types

more useful

...making them parametrically polymorphic

trait ListI { def ::(value: Int) = ConsI(value, this) } case object NilI extends ListI case class ConsI(value: Int, list: ListI) extends ListI

BEFORE:

...making them parametrically polymorphic

sealed trait ListG[+A] { def ::[B >: A](value: B) = ConsG[B](value, this) } case object NilG extends ListG[Nothing] case class ConsG[A](head: A, tail: ListG[A]) extends ListG[A]

AFTER:

Defining a simple, product algebraic type for binary tree is a no-brainer:

sealed trait BTreeG[+A] case class Tip[A](value: A) extends BTreeG[A]

case class Bin[A](left: BTreeG[A], right: BTreeG[A]) extends BTreeG[A]

ListG and BTreeG are Generalized Algebraic Data Types

And the programming approach itself is called Generic

Programming

Let's say, we want to find a sum of all the elements in the ListG and BTreeG, now...

Let's say, we want to find a sum of all the elements in the ListG and BTreeG, now...

fold

def foldL[B](n: B)(f: (B, A) => B) (list: ListG[A]): B = list match { case NilG => n case ConsG(head, tail) => f(foldL(n)(f)(tail), head) } }

foldL[Int, Int](0)(_ + _)(list)

def foldT[B](f: A => B)(g: (B, B) => B) (tree: BTreeG[A]): B = tree match { case Tip(value) => f(value) case Bin(left, right) => g(foldT(f)(g)(tree),foldT(f)(g)(tree)) }

foldT[Int, Int](0)(x => x)(_ + _)(tree)

Obviously, foldL and foldT have very much in common.

Obviously, foldL and foldT have very much in common.

In fact, the biggest difference is in

the shape of the data

That would be great, if we could abstract away from data type...

That would be great, if we could abstract away from data type...

With Datatype Generic programming we can!

Requirements:

• Fix data type (recursive data type) • Datatype-specific instance of

Bifunctor

1. Fix type

Fix [F [_, _], A]

Higher-kinded shape (pair, list, tree,...)

Type parameter of the shape

Let's create an instance of Fix for List shape

trait ListF[+A, +B] case object NilF extends ListF[Nothing, Nothing] case class ConsF[A, B](head: A, tail: B) extends ListF[A, B]

type List[A] = Fix[ListF, A]

2. Datatype-specific instance of Bifunctor

trait Bifunctor[F[_, _]] { def bimap[A, B, C, D](k: F[A, B], f: A => C, g: B => D): F[C, D] }

Defines mapping for the shape

Bifunctor instance for ListF

implicit val listFBifunctor = new Bifunctor[ListF]{ def bimap[A, B, C, D](k: ListF[A,B], f: A => C, g: B => D): ListF[C,D] = k match { case NilF => NilF case ConsF(head, tail) => ConsF(f(head), g(tail)) } }

It turns out, that a wide number of other generic operations on data types can be expressed via bimap!

def map[A, B, F [_, _]](f : A => B)(t : Fix [F, A]) (implicit ft : Bifunctor [F]) : Fix [F, B] def fold[A, B, F [_, _]](f : F[A, B] => B)(t : Fix[F,A]) (implicit ft : Bifunctor [F]) : B def unfold [A, B, F [_, _]] (f : B => F[A, B]) (b : B) (implicit ft : Bifunctor [F]) : Fix[F, A] def hylo [A, B, C, F [_, _]] (f : B => F[A, B]) (g : F[A, C] => C)(b: B) (implicit ft : Bifunctor [F]) : C def build [A, F [_, _]] (f : {def apply[B]: (F [A, B] => B) => B}): Fix[F, A]

See http://goo.gl/I4OBx

This approach is called

Origami patterns • Origami patterns can be applied to generic

data types!

• Include the following GoF patterns o Composite (algebraic data type itself) o Iterator (map) o Visitor (fold / hylo) o Builder (build / unfold)

Those operations are called

Origami patterns • The patterns can be applied to generic data

types!

• Include the following GoF patterns o Composite (algebraic data type itself) o Iterator (map) o Visitor (fold) o Builder (build / unfold / hylo)

30 loc

vs 250 loc in pure Java

Live demo! http://goo.gl/ysv5Y

Thanks for watching!

Recommended