View
4.360
Download
1
Category
Tags:
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