Haskell Design Pattern - Sherri Shulman - CppCon 2015
Haskell Design Pattern - Sherri Shulman - CppCon 2015
Behavior
Sherri Shulman
October 2, 2015
1 Introduction
2 Type Classes
4 Functors
5 Applicative Functors
Statically typed
Strongly typed
Referentially transparent
First class functions
Very expressive type language
Orthogonal language features
Clear semantics
No side effects
C++ vs Haskell
Although C++ is a very different language than Haskell, the language
features and design patterns in Haskell can promote ideas about how to
structure solutions. In some sense Haskell can become the ”design
language” and C++ the ”implementation language”
Haskell Features
1 Type classes These features are used to support
2 Algebraic data types genericity and high level abstractions
3 Functors that promote reuse and a particular
style of computation.
4 Monads
5 Continuations
> type L i s t x = [ x ]
> c l a s s HasEmpty x where
> empty : : x −> Bool
> c l a s s H a s h a b l e x where
> h as h : : x −> Bool
> i n s t a n c e H a s h a b l e I n t where
> h as h x = x
> i n s t a n c e H a s h a b l e k =>
> H a s h s e t ( IntMap ( L i s t k ) ) where
> type El em e n t ( IntMap ( L i s t k ) ) = k
> s i z e m = Data . IntMap . s i z e m
> h : : IntMap ( L i s t I n t )
> h = Data . IntMap . empty
> test = almostFull h
Full code: Figure 1
Sherri Shulman (TESC) Haskell Design Patterns title October 2, 2015 9 / 55
Type class notes
There are three type classes. We restrict the types x that model Hashset
to those types x that model HasEmpty and Hashabale.
This data type IntMap (List k)) is still not a concrete type since k is a
type parameter but so long as k models Hashable we are type correct and
type safe.
// c o n c e p t
c o n c e p t H a s h s e t <typename X> : HasEmpty<X> {
typename e l e m e n t ;
r e q u i r e s H a s h a b l e <e l e m e n t >;
i n t s i z e (X ) ;
}
// m o d e l l i n g
template<H a s h a b l e K>
c o n c e p t m a p H a s h s e t <intmap< l i s t <K>>> {
i n t s i z e ( intmap< l i s t <K>> m) { . . . }
}
// a l g o r i t h m
template<H a s h s e t T>
bool a l m o s t F u l l (T h ) { . . . }
// i n s t a n t i a t i o n
intmap< l i s t <i n t >> h ;
bool t e s t = a l m o s t F u l l (H ) ;
Here Hashset is a subclass of HasEmpty with an independent constraint
that element be Hashable.
For this example we’ll use a design and implementation of an evaluator for
a lambda calculus like language.
We’ll do this in two ways: one using standard algebraic data types and one
using GADTs (Generalized Algebraic Datatypes).
The C++ implementation uses classes and subclasses. Each of the kinds
of term is implemented in a subclass. In order to implement an evaluator,
each subclass will have its own eval method.
This is sometimes called the Row vs Column effect: in both styles we have
to change something. It’s easier to add behavior to the algebraic data type
design and harder to add a new kind of value. It’s easier to add a new
kind of value in the OOP design but harder to add new behavior.
A more expressive approach that embodies type safety, allows user defined
types, and is more dynamic is described in [Alexandrescu, 2002]:
template <c l a s s TList>
class Variant {
....
};
typedef Variant<
TYPELIST 4 ( t y p e V a r , typeAbs , typeApp , typeEmpty )
(Assuming that these user-defined types have been defined.)
The usual way to do this is to introduce a new data type for the values.
On the other hand, algebraic data types are less dynamic: we have to
consider all the kinds of values that may occur. Adding new kinds of
values is easier in a subclass hierarchy (potentially at increased cost in
adding behavior).
You can think (very generally) of a Functor as something that holds a value
and provides this mapping function. A list is an instance of a Functor:
> i n s t a n c e F u n c t o r [ ] where
> fmap = map
Here is a common type in Haskell that embodies the idea that a
computation may fail:
> d a t a Maybe a = J u s t a | N o t h i n g
(A computation either computes a value of type a or Nothing). The
Maybe type is a functor:
> i n s t a n c e F u n c t o r Maybe where
> fmap f ( J u s t x ) = J u s t ( f x )
> fmap f N o t h i n g = N o t h i n g
What might a Functor look like in C++? The following sketch is due to
Bartosz Milewski in [Bartosz Milewski. Jan 2015] and
[Bartosz Milewski. Sep 2012].
template<template<c l a s s > F , c l a s s A , c l a s s B>
F<B> fmap ( s t d : : f u n c t i o n <B(A) > , F<A>
template<c l a s s A , c l a s s B>
o p t i o n a l <B> fmap ( s t d : : f u n c t i o n <B(A)> f ,
o p t i o n a l <A> o p t ) {
i f ( ! o p t . i s V a l i d ( ) ) r e t u r n o p t i o n a l <B>{}
e l s e r e t u r n o p t i o n a l <B>{ f ( o p t . v a l ( ) ) } ;
} Sherri Shulman (TESC) Haskell Design Patterns title October 2, 2015 36 / 55
Functors cont
A note on partial application and curried vs uncurried functions. The
function fmap defined for a Functor f with type (a → b) → fa → fb
expects to get a function of one argument and a Functor holding a value
of type a. If I apply fmap to a function g (fmap g) we get a function of
type fa → fb. So in a sense fmap has transformed a function g of type
a → b to a function of type fa → fb. Examples:
fmap length [[1 ,2] , [3 ,4 ,5] , [5 ,6 ,7]]
fmap Just [1 ,2 ,3 ,4]
fmap chr [101 , 102 , 103]
fmap o r d ” abcd ”
In order to do partial application we require first-order functions and the
function must be curried form (a → b → c) rather than uncurried form
((a, b) → c. Using higher order functions we can move back and forth
between the curried and uncurried forms.
c u r r y f = \ a −> \b −> f ( a , )
u n c u r r y f = \ ( a , b ) −> f a b
Sherri Shulman (TESC) Haskell Design Patterns title October 2, 2015 37 / 55
Functor types
Using fmap we’ve been able to use a mapping function without knowing
any of the detail of its implementation so there would be no need to break
encapsulation, similar to the motivation for using an iterator pattern as
opposed to a loop. In addition the functor laws guarantee composition in a
very general way.
The operator h∗i is really just an apply operator for Functors. Thinking in
types again,
c o n c a t : : [ [ a ] ] −> [ a ]
((<∗>) ( J u s t c o n c a t ) ) : : Maybe [ [ a ] ] −> Maybe [ a ]
In order to move from a function a → b to a function f a → f b we can
use the function pure (it will lift any value, and wrap it in a functor,
functions included.)
> i n s t a n c e A p p l i c a t i v e [ ] where
> pure x = [ x ]
> f s <∗> x s = [ f x | f <− f s , x <− x s ]
These are somewhat less satisfying than the Haskell because they do
expose some of the underlying structure and require an explicit reference
to the wrapping effect.
A simple example:
> i n s t a n c e Monad Maybe where
> return x = Just x
> N o t h i n g >>= f = N o t h i n g
> J u s t x >>= f = f x
> fail = Nothing
So in the Maybe monad we can write a function f that examines the
contents of the Maybe monad does some computation, and then rewraps
it for further computation.