Recursive Datatypes
Outline
- What's Recursion?
- Ex. A Recursive Program
- Ex. Recursive Types
- Folds
- Guided Exercises
- Lists and Naturals
- Syntax Trees
- Labelled Trees
- Recursive, AST, and Annotated
- Applications of Recursive Datatypes
What's Recursion?
Properties of Recursion
- Recursion occurs when a function or type is defined in terms of itself
- Infinite recursion goes on forever
-
Non-infinite recursion terminates with a base case
- A scenario where there is no self-reference
- Steps must actually make progress towards base case in to terminate
Recursive Programs
An example
Sum of List - Iteratively
def sum(numbers: List[int]) -> int:
result = 0
for number in numbers:
result = result + number
return result
>>> sum([1,2,3])
6
>>> sum([10])
10
>>> sum([])
0
Sum of List - Recursively
def sum(numbers: List[int]) -> int:
if (numbers == []):
return 0
head: int = numbers[0]
tail: [int] = numbers[1:]
return head + sum(tail)
>>> sum([1,2,3])
6
>>> sum([10])
10
>>> sum([])
0
def sum(numbers: List[int]) -> int:
if (numbers == []):
return 0
head: int = numbers[0]
tail: [int] = numbers[1:]
return head + sum(tail)
Base Case
Self-reference/ recursive call
Sum of List - Recursively
sum :: [Int] -> Int
sum [] = 0
sum x:xs = x + (sum xs)
Sum of List - Recursively
Haskell edition
sum :: [Int] -> Int
sum [] = 0
sum x:xs = x + (sum xs)
Base Case
Self-reference/ recursive call
Sum of List - Recursively
Haskell edition
Recursive Types
An example
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
Lists and Naturals
data Natural =
One
| Succ Natural
-
Represent natural numbers (positive integers)
-
Example: (Python)
-
one: Nat = 1
# succ(nat: Nat) -> Nat
succ(one) == 2
succ(succ(one)) == 3
Naturals
data Natural =
One
| Succ Natural
natOne = One
natTwo = Succ One
natThree = Succ (Succ One)
natFour = Succ (Succ (Succ One))
alsoNatFour = Succ (natThree)
-
Represent natural numbers (positive integers)
-
Example: (Haskell)
-
Naturals
data List a =
Empty
| Cons a (List a)
-
This list is created by "cons-ing" onto the head of the list
-
Example: (Python)
-
num: int = 1
nums: List[int] = [2,3]
empty: List[Int] = []
# cons(head: int, tail: List[int]) -> List[int]
cons(num, empty) == [1]
cons(num, nums) == [1,2,3]
cons(num, cons(num, nums)) == [1,1,2,3]
Lists
data List a =
Empty
| Cons a (List a)
listOne = Cons 1 Empty
listOneTwo = Cons 1 (Cons 2 Empty)
listOneToThree = Cons 1 (Cons 2 (Cons 3 Empty))
listOneToFour = Cons 1 (Cons 2 (Cons 3 (Cons 4 Empty)))
listOneToFour' = 1 : 2 : 4 : []
listOneToFour'' = [1, 2, 3, 4]
-
This list is created by "cons-ing" onto the head of the list
-
Example: (Haskell)
-
Lists
Folds
Folds (Python)
def concat_strings(strings: List[str]) -> str:
return foldr_list(
operation=lambda a, b: a + b,
base_case="",
elements=strings)
>>> concat_strings(["Man", " it's", " a", " hot", " one"])
"Man it's a hot one"
A = TypeVar('A'); B = TypeVar('B')
def foldr_list(operation: Callable[[A, B], B],
base_case: B,
elements: List[A]
) -> B:
if (elements == []):
return base_case
head: A = elements[0]
tail: List[A] = elements[1:]
return operation(head, foldr_list(operation, base_case, tail))
Folds
-- Simplified right fold
foldrList :: (a -> b -> b) -> b -> [a] -> b
foldrList operation baseCase [] = baseCase
foldrList operation baseCase (x:xs) =
operation x (foldrList operation baseCase xs)
λ> concatStrings = foldrList (++) ""
λ> concatStrings ["Spanish", " Harlem", " Mona", " Lisa"]
"Spanish Harlem Mona Lisa"
λ> concatStrings []
""
sum :: [Int] -> Int
sum numbers = foldr (+) 0 numbers
λ> sum [1,2,3]
6
λ> sum [10]
10
λ> sum []
0
Sum - As a fold
sum :: [Int] -> Int
sum numbers = foldr (+) 0 numbers
Base Case
Self-reference/ recursive call
Sum - As a fold
Visualizing Folds
foldrList :: (a -> b -> b) -> b -> [a] -> b
foldrList f baseCase [x1, x2, x3]
=
----------------------------------
: -> f
/ \ / \
x1 : -> x1 f
/ \ / \
x2 : -> x2 f
/ \ / \
x3 [] -> x3 baseCase
sum :: [Int] -> Int
sum numbers = foldr (+) 0 numbers
Visualizing Sum
sum [1, 2, 3]
=
------------------------
: -> +
/ \ / \
1 : -> 1 +
/ \ / \
2 : -> 2 +
/ \ / \
3 [] -> 3 0
Guided Exercises
- Goal: Generalize similar recursive types into one most excellent and general recursive type
Strategies
- Find a way to visualize values and patterns
- Identify similar recursive datatypes and factor out similarities
- If we only have one type, mess with it until we have another!
- Try formulating in terms of primitive ADTs
- Tuple, Maybe, Either, Identity, Void, Unit
- Turn concrete types into polymorphic ones
- Call new patterns by new names
Generalizing
Lists and Naturals
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
Visualize?
data Natural =
One
| Succ Natural
Visualizing Naturals
| = One
/ \
One Succ (|) = Succ One
/ \
One Succ (|) = Succ (Succ One)
/ \
One Succ (|) = Succ (Succ (Succ One))
/ \
One Succ (|) = Succ (Succ (Succ (Succ One)))
/ \
One ... = Succ (Succ (Succ (Succ ...)))
Visualizing Lists
| = Empty
/ \
E Cons
1 (|) = Cons 1 Empty
/ \
E Cons
2 (|) = Cons 1 (Cons 2 Empty)
/ \
E Cons
3 (|) = Cons 1 (Cons 2 (Cons 3 Empty))
/ \
E Cons
4 (|) = Cons 1 (Cons 2 (Cons 3 (Cons 4 Empty)))
/ \
E Cons
5 (...) = Cons 1 (Cons 2 (Cons 3 (Cons 4 (Cons 5 ...))))
data List a =
Empty
| Cons a (List a)
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
What's similar?
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
Both have a "unit" value
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
Both have a recursive value
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
Generalized?
data Recursive a =
Unit
| Ctor (Recursive a)
?
Recursive can either be a "unit" value or a recursively defined value
Generalize?
?
data Recursive a =
Unit
data Natural =
One
data List a =
Empty
Isomorphic?
?=
one = One :: Natural
empty = Empty :: List Int
rOne = Unit :: Recursive ()
rEmpty = Unit :: Recursive Int
?
data Recursive a =
Unit
data Natural =
One
data List a =
Empty
Yes!
one = One :: Natural
empty = Empty :: List Int
rOne = Unit :: Recursive ()
rEmpty = Unit :: Recursive Int
data Recursive a =
Unit
data Natural =
One
data List a =
Empty
==
data Natural =
Succ Natural
data List a =
Cons a (List a)
Isomorphic?
data Recursive a =
Ctor (Recursive a)
?
Isomorphic?
rplusOne r = Ctor r :: Recursive ()
rconsOne r = Ctor r :: Recursive Int
rplusTwo r = Ctor (Ctor r)
rconsOneTwo r = Ctor (Ctor r)
plusOne n = Succ n :: Natural
consOne t = Cons 1 t :: List Int
plusTwo n = Succ (Succ n)
consOneTwo t = Cons 1 (Cons 2 t)
?=
data Natural =
Succ Natural
data List a =
Cons a (List a)
data Recursive a =
Ctor (Recursive a)
?
NO!
rplusOne r = Ctor r :: Recursive ()
rconsOne r = Ctor r :: Recursive Int
^?^
rplusTwo r = Ctor (Ctor r)
rconsOneTwo r = Ctor (Ctor r)
^?^ ^?^
plusOne n = Succ n :: Natural
consOne t = Cons 1 t :: List Int
^
plusTwo n = Succ (Succ n)
consOneTwo t = Cons 1 (Cons 2 t)
^ ^
!=
data Natural =
Succ Natural
data List a =
Cons a (List a)
data Recursive a =
Ctor (Recursive a)
?
Maybe Label?
data Recursive a =
Unit
| Ctor (Maybe a) (Recursive a)
?
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
data Recursive a =
Unit
| Ctor (Maybe a) (Recursive a)
type Nat = Recursive ()
goodOne = Unit
goodTwo = Ctor Nothing Unit
goodThree = Ctor Nothing (Ctor Nothing Unit)
Maybe Label?
?
Can we make a Natural?
data Recursive a =
Unit
| Ctor (Maybe a) (Recursive a)
type Nat = Recursive ()
goodOne = Unit
-- = One
goodTwo = Ctor Nothing Unit
-- = Succ One
goodThree = Ctor Nothing (Ctor Nothing Unit)
-- = Succ (Succ One)
Maybe Label?
Can we make a Natural?
YES
data Recursive a =
Unit
| Ctor (Maybe a) (Recursive a)
type List a = Recursive a
goodEmpty = Unit
goodOne = Ctor (Just 1) Unit
goodOneTwo = Ctor (Just 1) (Ctor (Just 2) Unit)
Maybe Label?
?
Can we make a List?
data Recursive a =
Unit
| Ctor (Maybe a) (Recursive a)
type List a = Recursive a
goodEmpty = Unit
-- = Empty
goodOne = Ctor (Just 1) Unit
-- = Cons 1 Empty
goodOneTwo = Ctor (Just 1) (Ctor (Just 2) Unit)
-- = Cons 1 (Cons 2 Empty)
Maybe Label?
Can we make a List?
YES
data Recursive a =
Unit
| Ctor (Maybe a) (Recursive a)
Maybe Label?
So we can make a natural and list!
But something is wrong...
type Nat = Recursive (Recursive Int)
badNat :: Nat
badNat = Ctor (Ctor (Just 666) Unit) Unit
type IntList = Recursive Int
badList :: IntList
badList = Ctor Nothing (Ctor (Just 1) Unit)
badEmpty :: IntList
badEmpty = Ctor Nothing (Ctor Nothing Unit)
🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
data Natural =
One
| Succ Natural
data List a =
Empty
| Cons a (List a)
Wrapper on Nodes?
data Recursive w a =
Unit
| Ctor (w (Recursive w a))
?
-- |List wrapper w/ Label!
data ListNode a r = ListNode
{ nodeValue :: a
, tailNodes :: r
}
-- |Nat wrapper w/o Label!
data NatNode r = NatNode
{ tailNodes :: r
}
type List a = Recursive (ListNode a) a
type Nat = Recursive NatNode ()
data Recursive w a =
Unit
| Ctor (w (Recursive w a))
?
type List a = Recursive (ListNode a) a
type Nat = Recursive NatNode ()
oneTwo = Ctor (ListNode 1 (Ctor (ListNode 2 Unit)))
two = Ctor (NatNode (Ctor (NatNode Unit)))
data Recursive w a =
Unit
| Ctor (w (Recursive w a))
?
type List a = Recursive (ListNode a) a
type Nat = Recursive NatNode ()
oneTwo = Ctor (ListNode 1 (Ctor (ListNode 2 Unit)))
-- Cons 1 (Cons 2 Unit)
two = Ctor (NatNode (Ctor (NatNode Unit)))
-- Succ (Succ Unit)
data Recursive w a =
Unit
| Ctor (w (Recursive w a))
Mission COMPLETE!!
data Recursive w a =
Unit
| Ctor (w (Recursive w a))
Or can we go further?
data Recursive w a =
Unit
| Ctor (w (Recursive w a))
Or can we go further?
-
Hint
- Recursive can be either a unit or a parameterized value
- Parameterized value is parameterized by a wrapper
Don't we already have a wrapper type that can be unit or a parameterized value?
Don't we already have a wrapper type that can be unit or a parameterized value?
data Maybe a =
Nothing
| Just a
data ListNode a r = ListNode
{ nodeValue :: a
, tailNodes :: Maybe r
}
data NatNode r = NatNode
{ tailNodes :: Maybe r
}
type List a = Recursive (ListNode a)
type Nat = Recursive NatNode
data Recursive w =
Ctor (w (Recursive w))
?
data NatNode r = NatNode
{ tailNodes :: Maybe r
}
type Nat = Recursive NatNode
one :: Nat
one = Ctor (NatNode Nothing)
succ :: Nat -> Nat
succ = Ctor . NatNode . Just
natOne = one
natTwo = succ one
natThree = succ (succ one)
natFour = succ (succ (succ one))
alsoNatFour = succ (natThree)
data Recursive w = Ctor (w (Recursive w))
data ListNode a r = ListNode
{ nodeValue :: a
, tailNodePtr :: Maybe r
}
type List a = Recursive (ListNode a)
empty :: List Int
empty = Ctor (ListNode 10 Nothing)
cons :: a -> List a -> List a
cons a = Ctor . ListNode a . Just
listOne = cons 1 empty
listOneTwo = cons 1 (cons 2 empty)
listOneToThree = cons 1 (cons 2 (cons 3 empty))
listOneToFour = cons 1 (cons 2 (cons 3 (cons 4 empty)))
data Recursive w = Ctor (w (Recursive w))
?
data ListNode a r = ListNode
{ nodeValue :: a
, tailNodePtr :: Maybe r
}
type List a = Recursive (ListNode a)
empty :: List Int
empty = Ctor (ListNode 10 Nothing)
^
cons :: a -> List a -> List a
cons a = Ctor . ListNode a . Just
data Recursive w = Ctor (w (Recursive w))
That's not empty!
What if we flip the wrapper?
data ListNode a r = ListNode
{ nodeValue :: a
, tailNodePtr :: r
}
type List a = Recursive (Maybe (ListNode a))
empty :: List a
empty = Ctor . Nothing
cons :: a -> List a -> List a
cons a = Ctor . ListNode a . Just
listOne = cons 1 empty
listOneTwo = cons 1 (cons 2 empty)
listOneToThree = cons 1 (cons 2 (cons 3 empty))
listOneToFour = cons 1 (cons 2 (cons 3 (cons 4 empty)))
data Recursive w = Ctor (w (Recursive w))
CAN WE GO DEEPER???
data NatF r =
OneF
| SuccF r
type Nat = Recursive NatF
one :: Nat
one = Ctor OneF
succ :: Nat -> Nat
succ = Ctor . SuccF
natOne = one
natTwo = succ one
natThree = succ (succ one)
natFour = succ (succ (succ one))
alsoNatFour = succ (natThree)
data Recursive w = Ctor (w (Recursive w))
data Natural =
One
| Succ Natural
Visualizing Naturals
| = One
/ \
One Succ (|) = Succ One
/ \
One Succ (|) = Succ (Succ One)
/ \
One Succ (|) = Succ (Succ (Succ One))
/ \
One Succ (|) = Succ (Succ (Succ (Succ One)))
/ \
One ... = Succ (Succ (Succ (Succ ...)))
Ctor = Ctor OneF
/ \
OneF SuccF (Ctor) = Ctor (SuccF (Ctor OneF))
/ \
OneF SuccF (Ctor) = Ctor (SuccF (Ctor (SuccF (Ctor OneF
/ \ ))))
OneF SuccF (Ctor) = Ctor (SuccF (Ctor (SuccF (Ctor (SuccF
/ \ (Ctor OneF)))))
OneF SuccF (Ctor) = Ctor (SuccF (Ctor (SuccF (Ctor (SuccF
/ \ (Ctor (SuccF (Ctor OneF))))))
OneF ... = Ctor (SuccF (Ctor (SuccF (Ctor (SuccF
(Ctor (SuccF (Ctor (SuccF
(Ctor OneF)))))))
data Recursive w = Ctor (w (Recursive w))
data NatF r =
OneF
| SuccF r
type Nat = Recursive NatF
one :: Nat
one = Ctor OneF
succ :: Nat -> Nat
succ num = Ctor (SuccF num)
data ListF a r =
EmptyF
| ConsF a r
type List = Recursive (ListF a)
empty :: List
empty = Ctor EmptyF
cons :: a -> List -> List
cons = Ctor . ConsF
listOne = cons 1 empty
listOneTwo = cons 1 (cons 2 empty)
listOneToThree = cons 1 (cons 2 (cons 3 empty))
listOneToFour = cons 1 (cons 2 (cons 3 (cons 4 empty)))
data Recursive w = Ctor (w (Recursive w))
Visualizing Lists
| = Empty
/ \
E Cons
1 (|) = Cons 1 Empty
/ \
E Cons
2 (|) = Cons 1 (Cons 2 Empty)
/ \
E Cons
3 (|) = Cons 1 (Cons 2 (Cons 3 Empty))
/ \
E Cons
4 (|) = Cons 1 (Cons 2 (Cons 3 (Cons 4 Empty)))
/ \
E Cons
5 (...) = Cons 1 (Cons 2 (Cons 3 (Cons 4 (Cons 5 ...))))
data List a =
Empty
| Cons a (List a)
data ListF a r =
EmptyF
| ConsF a r
type List = Recursive (ListF a)
empty :: List
empty = Ctor EmptyF
cons :: a -> List -> List
cons head tail =
Ctor (ConsF head tail)
data Recursive w = Ctor (w (Recursive w))
Ctor = Ctor EmptyF
/ \
EmpF ConsF
1 (Ctor) = Ctor (ConsF 1 (Ctor EmptyF))
/ \
EmpF ConsF
2 (Ctor) = Ctor (ConsF 1 (Ctor (ConsF 2 (Ctor EmptyF
/ \ ))))
EmpF ConsF
3 (Ctor) = Ctor (ConsF 1 (Ctor (ConsF 2 (Ctor (ConsF 3
/ \ (Ctor EmptyF)))))
EmpF ConsF
4 (Ctor) = Ctor (ConsF 1 (Ctor (ConsF 2 (Ctor (ConsF 3
/ \ (Ctor (ConsF 4 (Ctor EmptyF))))))
EmpF ... = Ctor (ConsF 1 (Ctor (ConsF 2 (Ctor (ConsF 3
(Ctor (ConsF 4 (Ctor (ConsF ...
Takeaways
- Two ways of using Recursive:
- Compose Different Wrappers
- Different wrappers give us different powers
-
Maybe
- Terminates recursion
-
Tuple
- Labelled layers/nodes of recursion
-
Identity
- Infinite recursion
-
Maybe
- Different wrappers give us different powers
- Compose Different Wrappers
Takeaways
- Two ways of using Recursive:
- Pattern Functor
- We can our own wrapper type with terms that make sense to us
- Steps:
- Lift original type (parameterize)
- Replace recursive value with continuation
- Pattern Functor
Takeaways
-
Recursive captures a pattern of recursion we can use to make recursive types
- Like the fold function let's us have recursive function without using explicit recursion, Recursive let's us have recursive type with using explicit recursion
Generalizing
Leaf Valued Trees
Trees
with values at leaves
- Let's look at a tree that holds values at the leaves:
- Leaves (values)
- Branches
- Recursion
- Leaves (values)
- Branches
- Exactly 2
- Recursion
data Tree
= Leaf Int
| Branch Tree Tree
Binary Trees
with values at leaves
data Tree
= Leaf Int
| Branch Tree Tree
someTree :: Tree
someTree =
Branch
(Branch
(Branch (Leaf 1) (Leaf 2))
(Branch (Leaf 3) (Leaf 4))
)
(Leaf 5)
-- •
-- / \
-- • \
-- / \ \
-- • • \
-- / \ / \ \
-- 1 2 3 4 5
someValues = (1,2,3,4,5)
someTree :: Tree
someTree =
Branch
(Branch
(Branch (Leaf 1) (Leaf 2))
(Branch (Leaf 3) (Leaf 4))
)
(Leaf 5)
-- •
-- / \
-- • \
-- / \ \
-- • • \
-- / \ / \ \
-- 1 2 3 4 5
someTree :: Tree
someTree =
Branch
(Branch
(Branch (Leaf 1) (Leaf 2))
(Branch (Leaf 3) (Leaf 4))
)
(Leaf 5)
-- •
-- / \
-- • \
-- / \ \
-- • • \
-- / \ / \ \
-- 1 2 3 4 5
someStructuredValues = (((1, 2), (3, 4)), 5)
someList = (1, 2, 3, 4, 5)
-- •
-- / \
-- • \
-- / \ \
-- • • \
-- / \ / \ \
-- 1 2 3 4 5
-- (,)
-- / \
-- (,) \
-- / \ \
-- (,) (,) \
-- / \ / \ \
-- 1 2 3 4 5
someStructuredValues = (((1, 2), (3, 4)), 5)
-- +
-- / \
-- - \
-- / \ \
-- * + \
-- / \ / \ \
-- 1 2 3 4 5
someExpression = ((1 * 2) - (3 + 4)) + 5
-- Arithmetic Expressions
data Math
= Lit Int
| Add Math Math
| Sub Math Math
| Mul Math Math
someMath :: Math
someMath =
Add
(Sub
(Mul (Lit 1) (Lit 2))
(Add (Lit 3) (Lit 4))
)
(Lit 5)
-- ((1 * 2) - (3 + 4)) + 5
-- +
-- / \
-- - \
-- / \ \
-- * + \
-- / \ / \ \
-- 1 2 3 4 5
data Tree
= Leaf Int
| Branch Tree Tree
data Math
= Lit Int
| Add Math Math
| Sub Math Math
| Mul Math Math
| Neg Math
data Tree a
= Leaf a
| Branch (Tree a) (Tree a)
data Math a
= Lit a
| Add (Math a) (Math a)
| Sub (Math a) (Math a)
| Mul (Math a) (Math a)
| Neg (Math a)
data Tree a
= Leaf a
| Branch (Tree a) (Tree a)
data Math a
= Lit a
| Add (Math a) (Math a)
| Sub (Math a) (Math a)
| Mul (Math a) (Math a)
| Neg (Math a)
Both have a leaf element
data Tree a
= Leaf a
| Branch (Tree a) (Tree a)
data Math a
= Lit a
| Add (Math a) (Math a)
| Sub (Math a) (Math a)
| Mul (Math a) (Math a)
| Neg (Math a)
Both have a branches of
sub-trees/sub-expressions
data Tree a
= Leaf a
| Branch (Pair a)
data Pair a = Pair (Tree a) (Tree a)
data Math a
= Lit a
| Op (Operator a)
data Operator a
= Add (Math a) (Math a)
| Sub (Math a) (Math a)
| Mul (Math a) (Math a)
| Neg (Math a)
data Tree a
= Leaf a
| Branch (Pair (Tree a))
data Pair a = Pair a a
data Math a
= Lit a
| Op (Operator (Math a))
data Operator a
= Add a a
| Sub a a
| Mul a a
| Neg a
data Tree a
= Leaf a
| Branch (Pair (Tree a))
data Pair a = Pair a a
data Math a
= Lit a
| Op (Operator (Math a))
data Operator a
= Add a a
| Sub a a
| Mul a a
| Neg a
data Tree a
= Leaf a
| Branch (Pair (Tree a))
data Pair a = Pair a a
data Math a
= Lit a
| Op (Operator (Math a))
data Operator a
= Add a a
| Sub a a
| Mul a a
| Neg a
type Tree = AST Pair Int
data Pair a = Pair a a
data Math = AST Operator Int
data Operator a
= Add a a
| Sub a a
| Mul a a
| Neg a
data AST op a
= Leaf a
| Branch (op (AST op a))
Generic Abstract Syntax Tree
data Math
= Lit Int
| Add Math Math
| Sub Math Math
| Mul Math Math
someMath :: Math
someMath =
Add
(Sub
(Mul (Lit 1) (Lit 2))
(Add (Lit 3) (Lit 4))
)
(Lit 5)
someAstMath :: Math
someAstMath =
Branch
(Add
(Branch
(Sub
(Branch
(Mul (Leaf 1) (Leaf 2))
)
(Branch
(Add (Leaf 3) (Leaf 4))
)
)
)
(Leaf 5)
)
type Math = AST Operator Int
data Operator a
= Add a a
| Sub a a
| Mul a a
Old
New
data Math
= Lit Int
| Add Math Math
| Sub Math Math
| Mul Math Math
someMath :: Math
someMath =
Add
(Sub
(Mul (Lit 1) (Lit 2))
(Add (Lit 3) (Lit 4))
)
(Lit 5)
type Math = AST Operator Int
data Operator a
= Add a a
| Sub a a
| Mul a a
Old
New
someAstMath :: Math
someAstMath =
Branch
(Add
(Branch
(Sub
(Branch
(Mul (Leaf 1) (Leaf 2))
)
(Branch
(Add (Leaf 3) (Leaf 4))
)
)
)
(Leaf 5)
)
Simple Language
An Example
let addOne = (\a -> a + 1)
in addOne 42
addOne = lambda a: a + 1
addOne(42)
Haskell
Python
- Let's make a simple language that can express the following:
type SimpleLang = AST Operation Term
newtype Identifier = String
data Operation a
= LetIn Identifier a a -- Variable Binding
| Function Identifier a -- Function Abstraction
| Apply a a -- Apply Two Expressions
data Term
= Var Identifier
| Num Number
Simple ASTs for Simple Langs
let addOne = (\a -> a + 1)
in addOne 42
addOne = lambda a: a + 1
addOne(42)
Haskell
Python
addOne :: SimpleLang
addOne =
(Branch
(LetIn "addOne"
(Branch
(Function "a"
(Apply
(Branch
(Apply
(Leaf (Var "+"))
(Leaf (Var "a"))))
(Leaf (Num 1))))))
(Branch
(Apply
(Leaf (Var "addOne"))
(Leaf (Num 42))))))
Generalizing
Labelled Trees
data Rose a
Rose Tree
with values at all nodes
-
A rose tree is an n-ary tree
-
Each node has zero or more children, all of which are themselves rose trees
-
Unbounded number of branches
-
Value at every node (including branches)
-
-
type List a = [a]
data Rose a
= Leaf a
| Branch (List (Rose a))
[ Branch
[ Branch
[ Branch
[ Leaf 1
, Leaf 2
]
, Branch
[ Leaf 3
, Leaf 4
]
]
, Leaf 5
, Leaf 6
, Leaf 7
]
-- [ ]---
-- / \ \ \
-- [ ] \ \ \
-- / \ \ \ \
-- [ ] [ ] \ \ \
-- / \ / \ \ \ \
-- 1 2 3 4 5 6 7
[ [ [1,2]
, [3,4]
]
, 5
, 6
, 7
]
type List a = [a]
data Rose a
= Leaf a
| Branch (List (Rose a))
[ Branch
[ Branch
[ Branch
[ Leaf 1
, Leaf 2
]
, Branch
[ Leaf 3
, Leaf 4
]
]
, Leaf 5
, Leaf 6
, Leaf 7
]
-- ?----
-- / \ \ \
-- ? \ \ \
-- / \ \ \ \
-- ? ? \ \ \
-- / \ / \ \ \ \
-- 1 2 3 4 5 6 7
- But we said Rose Trees should be labelled at all nodes?
type List a = [a]
data Rose a
= Leaf a
| Branch ? (List (Rose a))
[ Branch ?
[ Branch ?
[ Branch ?
[ Leaf 1
, Leaf 2
]
, Branch ?
[ Leaf 3
, Leaf 4
]
]
, Leaf 5
, Leaf 6
, Leaf 7
]
-- ?----
-- [ / \ \ \ ]
-- ? \ \ \
-- [ / \ ] \ \ \
-- ? ? \ \ \
-- [/ \] [/ \] \ \ \
-- 1 2 3 4 5 6 7
( ?
, [( ?
, [ (?, [1,2])
, (?, [3,4])
]
, 5
, 6
, 7
]
)
type List a = [a]
data Rose a
= Leaf a
| Branch a (List (Rose a))
[ Branch 200
[ Branch 300
[ Branch 400
[ Leaf 1
, Leaf 2
]
, Branch 500
[ Leaf 3
, Leaf 4
]
]
, Leaf 5
, Leaf 6
, Leaf 7
]
-- 200---
-- [ / \ \ \ ]
-- 300 \ \ \
-- [ / \ ] \ \ \
-- 400 500 \ \ \
-- [/ \] [/ \] \ \ \
-- 1 2 3 4 5 6 7
( 200
, [ (300
, [(400, [1, 2])
,(500, [3, 4])
]
)
, 5
, 6
, 7
]
)
type List a = [a]
data Rose a
= Leaf a
| Branch a (List (Rose a))
[ Branch 200
[ Branch 300
[ Branch 400
[ Leaf 1
, Leaf 2
]
, Branch 500
[ Leaf 3
, Leaf 4
]
]
, Leaf 5
, Leaf 6
, Leaf 7
]
-- 200---
-- [ / \ \ \ ]
-- 300 \ \ \
-- [ / \ ] \ \ \
-- 400 500 \ \ \
-- [/ \] [/ \] \ \ \
-- 1 2 3 4 5 6 7
- What's the difference?
- Labelled Leaf
- Labelled branch with an empty subtree
type List a = [a]
data Rose a
= Leaf a
| Branch a (List (Rose a))
( 200
, [ (300
, [ (400, [ 1, 2])
, (500, [ 3, 4])
]
)
, 5
, 6
, 7
]
)
[ Branch 200
[ Branch 300
[ Branch 400
[ Leaf 1
, Leaf 2
]
, Branch 500
[ Leaf 3
, Leaf 4
]
]
, Leaf 5
, Leaf 6
, Leaf 7
]
-- 200---
-- [ / \ \ \ ]
-- 300 \ \ \
-- [ / \ ] \ \ \
-- 400 500 \ \ \
-- [/ \] [/ \] \ \ \
-- 1 2 3 4 5 6 7
type List a = [a]
data Rose a
= Branch a (List (Rose a))
( 200
, [ ( 300
, [ (400, [ (1, [])
, (2, [])
]
)
, (500, [ (3, [])
, (4, [])
]
)
]
)
, (5, [])
, (6, [])
, (7, [])
]
)
[ Branch 200
[ Branch 300
[ Branch 400
[ Branch 1 []
, Branch 2 []
]
, Branch 500
[ Branch 3 []
, Branch 4 []
]
]
, Branch 5 []
, Branch 6 []
, Branch 7 []
]
-- 200---
-- [ / \ \ \ ]
-- 300 \ \ \
-- [ / \ ] \ \ \
-- 400 500 \ \ \
-- [/ \] [/ \] \ \ \
-- 1 2 3 4 5 6 7
-- [] [] [] [] [] [] []
Generalized Annotated Trees
-- Annotated
data Annotated f a
= Ann a (f (Annotated f a))
-- equivalent to above
data Annotated' f a = Annotated'
{ annotation :: a
, annSubTree :: f (Annotated' f a)
}
Generalized Annotated Trees
type RoseTree a = Annotated [] a
type Trie a = Annotated (StrMap) a
type List a = Annotated (Identity) a
type DecisionTree a r = Annotated ((->) r) a
type BST a = Annotated (Compose Pair Maybe) a
type MerkleTree a = Annotated (HashTrie) a
Holy Toledo! So Versatile!
Fix, Free, and Cofree
Reviewing our Findings
- Shape of recursive data types
- Recursive
- AST
- Annotated
data AST f a
= Leaf a
| Branch (f (Tree a))
data Annotated f a
= Annotated a (f (Annotated f a))
-- equivalent to `Annotated`
data Annotated2 f a = Annotated2
{ annotation :: a
, annSubTree :: f (Annotated2 f a)
}
AST
- A value OR a branch
Annotated
- A value AND a branch
-- Free
data Free f a
= Pure a
| Rollup (f (Free a))
-- Cofree
data Cofree f a
= Cofree a (f (Cofree f a))
-- equivalent to `Cofree`
data Cofree2 f a = Cofree2
{ annotation :: a
, annSubTree :: f (Cofree2 f a)
}
Free
- A value OR a branch
Cofree
- A value AND a branch
data Free f a
= Pure a
| Rollup (f (Free a))
data Cofree f a
= a <: (f (Cofree f a))
Free
- A value OR a branch
Cofree
- A value AND a branch
Recursive
data Recursive w = Ctor (w (Recursive w))
Fix
newtype Fix f
= Fix (f (Fix f))
A fixed point (or fixpoint) is a point that, when provided to a function, yields as a result that same point.
- A fixed point of a function/type is a point that won't change under repeated application of the function/type.
newtype Fix f
= Fix (f (Fix f))
data Free f a
= Pure a
| Rollup (f (Free a))
data Cofree f a
= a <: (f (Cofree f a))
Fix, Free, and Cofree
Applications and Further Reading
Specification and Interpretation
with Free DSLs
Recursion Schemes
Extensible Effects
Freer-Simple
Fused Effects
Functor Combinators
Functor Combinators
- Fix, Free, and CoFree generalize recursion with datatypes that wrap every layer of recursion with some constructor
- Enables generic operations that know what to do with those constructors
freeMath :: Fix MathF
freeMath =
Free
(AddF
(Free
(SubF
(Free
(MulF
(Pure
(1))
(Pure
(2))))
(Free
(AddF
(Pure
(3))
(Pure
(4)))))
(Pure
(5)))
fixList :: Fix ListF
fixList =
Fix
(ConsF 10
(Fix
(ConsF 10
(Fix
(ConsF 10
(Fix
(NilF))))))))
Functor Combinators
- But can we generalize behaviors of our "Wrapper" type variable f to do more?
- Yes!
- Generalize your use of functors composition!
- Constrain with functor-combinators!
Functor Combinators
- Functor Sums
-
Higher-order Either
-
Provide either case, and user has to handle both possibilities
-
data (f :+: g) a
= L1 (f a)
| R1 (g a)
type MbNatural = Fix Maybe
type SumNatural f g = Fix (Identity :+: Proxy)
type List a = Fix ((,) a :+: Proxy)
mbThree = Fix . Just . Fix . Just $ Fix Nothing :: MbNat
sumThree = Fix . L1 . Identity . Fix . L1 . Identity . Fix $ R1 Proxy :: SumNat
oneTwoThree = Fix . L1 . (,) 1 . Fix . L1 . (,) 2
. Fix . L1 . (,) 3 . Fix $ R1 Proxy :: List Int -- ~= [1,2,3]
Functor Combinators
- Functor Products
-
Higher-order Tuple
-
Pairs wrappers, and let's user can choose which one they want to use
-
data (f :*: g) a = f a :*: g a
References
Blog
A Tour of Some Useful Recursive Types
Video
PS Unscripted - Free From Tree & Halogen VDOM
- Stole for last two exercises
Recursive Datatypes
By Heneli Kailahi
Recursive Datatypes
Explores basic recursive types in Haskell
- 263