The background is that the head of the Software Department of the Institute of Cybernetics, Ahto Kalja, recently received the Order of the White Star, 4th class from the President of Estonia. On this account, Estonian TV conducted an interview with him, during which they recorded also parts of my notes that were still present on the whiteboard in our coffee room.
You can watch the video online. The relevant part, which is about e-government, is from 18:14 to 21:18. I enjoyed it very much hearing Ahto Kalja’s colleague Arvo Ott talking about electronic tax returns and seeing some formula about limits immediately afterwards. At 20:38, there is also some Haskell-like pseudocode.
Like Haskell, Curry has support for literate programming. So I wrote this blog post as a literate Curry file, which is available for download. If you want to try out the code, you have to install the Curry system KiCS2. The code uses the functional patterns language extension, which is only supported by KiCS2, as far as I know.
The functional fragment of Curry is very similar to Haskell. The only fundamental difference is that Curry does not support type classes.
Let us do some functional programming in Curry. First, we define a type whose values denote me and some of my relatives.
data Person = Paul
| Joachim
| Rita
| Wolfgang
| Veronika
| Johanna
| Jonathan
| Jaromir
Now we define a function that yields the father of a given person if this father is covered by the Person
type.
father :: Person -> Person
father Joachim = Paul
father Rita = Joachim
father Wolfgang = Joachim
father Veronika = Joachim
father Johanna = Wolfgang
father Jonathan = Wolfgang
father Jaromir = Wolfgang
Based on father
, we define a function for computing grandfathers. To keep things simple, we only consider fathers of fathers to be grandfathers, not fathers of mothers.
grandfather :: Person -> Person
grandfather = father . father
Logic programming languages like Prolog are able to search for variable assignments that make a given proposition true. Curry, on the other hand, can search for variable assignments that make a certain expression defined.
For example, we can search for all persons that have a grandfather according to the above data. We just enter
grandfather person where person free
at the KiCS2 prompt. KiCS2 then outputs all assignments to the person
variable for which grandfather person
is defined. For each of these assignments, it additionally prints the result of the expression grandfather person
.
Functions in Curry can actually be non-deterministic, that is, they can return multiple results. For example, we can define a function element
that returns any element of a given list. To achieve this, we use overlapping patterns in our function definition. If several equations of a function definition match a particular function application, Curry takes all of them, not only the first one, as Haskell does.
element :: [el] -> el
element (el : _) = el
element (_ : els) = element els
Now we can enter
element "Hello!"
at the KiCS2 prompt, and the system outputs six different results.
We have already seen how to combine functional and logic programming with Curry. Now we want to do pure logic programming. This means that we only want to search for variable assignments, but are not interested in expression results. If you are not interested in results, you typically use a result type with only a single value. Curry provides the type Success
with the single value success
for doing logic programming.
Let us write some example code about routes between countries. We first introduce a type of some European and American countries.
data Country = Canada
| Estonia
| Germany
| Latvia
| Lithuania
| Mexico
| Poland
| Russia
| USA
Now we want to define a relation called borders
that tells us which country borders which other country. We implement this relation as a function of type
Country -> Country -> Success
that has the trivial result success
if the first country borders the second one, and has no result otherwise.
Note that this approach of implementing a relation is different from what we do in functional programming. In functional programming, we use Bool
as the result type and signal falsity by the result False
. In Curry, however, we signal falsity by the absence of a result.
Our borders
relation only relates countries with those neighbouring countries whose names come later in alphabetical order. We will soon compute the symmetric closure of borders
to also get the opposite relationships.
borders :: Country -> Country -> Success
Canada `borders` USA = success
Estonia `borders` Latvia = success
Estonia `borders` Russia = success
Germany `borders` Poland = success
Latvia `borders` Lithuania = success
Latvia `borders` Russia = success
Lithuania `borders` Poland = success
Mexico `borders` USA = success
Now we want to define a relation isConnected
that tells whether two countries can be reached from each other via a land route. Clearly, isConnected
is the equivalence relation that is generated by borders
. In Prolog, we would write clauses that directly express this relationship between borders
and isConnected
. In Curry, on the other hand, we can write a function that generates an equivalence relation from any given relation and therefore does not only work with borders
.
We first define a type alias Relation
for the sake of convenience.
type Relation val = val -> val -> Success
Now we define what reflexive, symmetric, and transitive closures are.
reflClosure :: Relation val -> Relation val
reflClosure rel val1 val2 = rel val1 val2
reflClosure rel val val = success
symClosure :: Relation val -> Relation val
symClosure rel val1 val2 = rel val1 val2
symClosure rel val2 val1 = rel val1 val2
transClosure :: Relation val -> Relation val
transClosure rel val1 val2 = rel val1 val2
transClosure rel val1 val3 = rel val1 val2 &
transClosure rel val2 val3
where val2 free
The operator &
used in the definition of transClosure
has type
Success -> Success -> Success
and denotes conjunction.
We define the function for generating equivalence relations as a composition of the above closure operators. Note that it is crucial that the transitive closure operator is applied after the symmetric closure operator, since the symmetric closure of a transitive relation is not necessarily transitive.
equivalence :: Relation val -> Relation val
equivalence = reflClosure . transClosure . symClosure
The implementation of isConnected
is now trivial.
isConnected :: Country -> Country -> Success
isConnected = equivalence borders
Now we let KiCS2 compute which countries I can reach from Estonia without a ship or plane. We do so by entering
Estonia `isConnected` country where country free
at the prompt.
We can also implement a nondeterministic function that turns a country into the countries connected to it. For this, we use a guard that is of type Success
. Such a guard succeeds if it has a result at all, which can only be success
, of course.
connected :: Country -> Country
connected country1
| country1 `isConnected` country2 = country2
where country2 free
Curry has a predefined operator
=:= :: val -> val -> Success
that stands for equality.
We can use this operator, for example, to define a nondeterministic function that yields the grandchildren of a given person. Again, we keep things simple by only considering relationships that solely go via fathers.
grandchild :: Person -> Person
grandchild person
| grandfather grandkid =:= person = grandkid
where grandkid free
Note that grandchild
is the inverse of grandfather
.
Functional patterns are a language extension that allows us to use ordinary functions in patterns, not just data constructors. Functional patterns are implemented by KiCS2.
Let us look at an example again. We want to define a function split
that nondeterministically splits a list into two parts.^{1} Without functional patterns, we can implement splitting as follows.
split' :: [el] -> ([el],[el])
split' list | front ++ rear =:= list = (front,rear)
where front, rear free
With functional patterns, we can implement splitting in a much simpler way.
split :: [el] -> ([el],[el])
split (front ++ rear) = (front,rear)
As a second example, let us define a function sublist
that yields the sublists of a given list.
sublist :: [el] -> [el]
sublist (_ ++ sub ++ _) = sub
In the grandchild
example, we showed how we can define the inverse of a particular function. We can go further and implement a generic function inversion operator.
inverse :: (val -> val') -> (val' -> val)
inverse fun val' | fun val =:= val' = val where val free
With this operator, we could also implement grandchild
as inverse grandfather
.
Inverting functions can make our lives a lot easier. Consider the example of parsing. A parser takes a string and returns a syntax tree. Writing a parser directly is a non-trivial task. However, generating a string from a syntax tree is just a simple functional programming exercise. So we can implement a parser in a simple way by writing a converter from syntax trees to strings and inverting it.
We show this for the language of all arithmetic expressions that can be built from addition, multiplication, and integer constants. We first define types for representing abstract syntax trees. These types resemble a grammar that takes precedence into account.
type Expr = Sum
data Sum = Sum Product [Product]
data Product = Product Atom [Atom]
data Atom = Num Int | Para Sum
Now we implement the conversion from abstract syntax trees to strings.
toString :: Expr -> String
toString = sumToString
sumToString :: Sum -> String
sumToString (Sum product products)
= productToString product ++
concatMap ((" + " ++) . productToString) products
productToString :: Product -> String
productToString (Product atom atoms)
= atomToString atom ++
concatMap ((" * " ++) . atomToString) atoms
atomToString :: Atom -> String
atomToString (Num num) = show num
atomToString (Para sum) = "(" ++ sumToString sum ++ ")"
Implementing the parser is now extremely simple.
parse :: String -> Expr
parse = inverse toString
KiCS2 uses a depth-first search strategy by default. However, our parser implementation does not work with depth-first search. So we switch to breadth-first search by entering
:set bfs
at the KiCS2 prompt. Now we can try out the parser by entering
parse "2 * (3 + 4)"
.
Note that our split
function is not the same as the split
function in Curry’s List
module.↩
Let me first describe the MU puzzle shortly. The puzzle deals with strings that may contain the characters , , and . We can derive new strings from old ones using the following rewriting system:
The question is whether it is possible to turn the string into the string using these rules.
You may want to try to solve this puzzle yourself, or you may want to look up the solution on the Wikipedia page.
The code is not only concerned with deriving from , but with derivations as such.
We import Data.List
:
import Data.List
We define the type Sym
of symbols and the type Str
of symbol strings:
data Sym = M | I | U deriving Eq
type Str = [Sym]
instance Show Sym where
show M = "M"
show I = "I"
show U = "U"
showList str = (concatMap show str ++)
Next, we define the type Rule
of rules as well as the list rules
that contains all rules:
data Rule = R1 | R2 | R3 | R4 deriving Show
rules :: [Rule]
rules = [R1,R2,R3,R4]
We first introduce a helper function that takes a string and returns the list of all splits of this string. Thereby, a split of a string str
is a pair of strings str1
and str2
such that str1 ++ str2 == str
. A straightforward implementation of splitting is as follows:
splits' :: Str -> [(Str,Str)]
splits' str = zip (inits str) (tails str)
The problem with this implementation is that walking through the result list takes quadratic time, even if the elements of the list are left unevaluated. The following implementation solves this problem:
splits :: Str -> [(Str,Str)]
splits str = zip (map (flip take str) [0 ..]) (tails str)
Next, we define a helper function replace
. An expression replace old new str
yields the list of all strings that can be constructed by replacing the string old
inside str
by new
.
replace :: Str -> Str -> Str -> [Str]
replace old new str = [front ++ new ++ rear |
(front,rest) <- splits str,
old `isPrefixOf` rest,
let rear = drop (length old) rest]
We are now ready to implement the function apply
, which performs rule application. This function takes a rule and a string and produces all strings that can be derived from the given string using the given rule exactly once.
apply :: Rule -> Str -> [Str]
apply R1 str | last str == I = [str ++ [U]]
apply R2 (M : tail) = [M : tail ++ tail]
apply R3 str = replace [I,I,I] [U] str
apply R4 str = replace [U,U] [] str
apply _ _ = []
Now we want to build derivation trees. A derivation tree for a string str
has the following properties:
str
.str
by a single rule application.We first define types for representing derivation trees:
data DTree = DTree Str [DSub]
data DSub = DSub Rule DTree
Now we define the function dTree
that turns a string into its derivation tree:
dTree :: Str -> DTree
dTree str = DTree str [DSub rule subtree |
rule <- rules,
subStr <- apply rule str,
let subtree = dTree subStr]
A derivation is a sequence of strings with rules between them such that each rule takes the string before it to the string after it. We define types for representing derivations:
data Deriv = Deriv [DStep] Str
data DStep = DStep Str Rule
instance Show Deriv where
show (Deriv steps goal) = " " ++
concatMap show steps ++
show goal ++
"\n"
showList derivs
= (concatMap ((++ "\n") . show) derivs ++)
instance Show DStep where
show (DStep origin rule) = show origin ++
"\n-> (" ++
show rule ++
") "
Now we implement a function derivs
that converts a derivation tree into the list of all derivations that start with the tree’s root label. The function derivs
traverses the tree in breadth-first order.
derivs :: DTree -> [Deriv]
derivs tree = worker [([],tree)] where
worker :: [([DStep],DTree)] -> [Deriv]
worker tasks = rootDerivs tasks ++
worker (subtasks tasks)
rootDerivs :: [([DStep],DTree)] -> [Deriv]
rootDerivs tasks = [Deriv (reverse revSteps) root |
(revSteps,DTree root _) <- tasks]
subtasks :: [([DStep],DTree)] -> [([DStep],DTree)]
subtasks tasks = [(DStep root rule : revSteps,subtree) |
(revSteps,DTree root subs) <- tasks,
DSub rule subtree <- subs]
Finally, we implement the function derivations
which takes two strings and returns the list of those derivations that turn the first string into the second:
derivations :: Str -> Str -> [Deriv]
derivations start end
= [deriv | deriv@(Deriv _ goal) <- derivs (dTree start),
goal == end]
You may want to enter
derivations [M,I] [M,U,I]
at the GHCi prompt to see the derivations
function in action. You can also enter
derivations [M,I] [M,U]
to get an idea about the solution to the MU puzzle.
Constraint
kind. In this blog post, I will show some examples of how this new feature can be used. This is a write-up of my Theory Lunch talk from 7 February 2013. The source of this article is a literate Haskell file, which you can download and load into GHCi.
The example code in this article needs support for the Constraint
kind, of course. So we have to enable the appropriate language extension (which is surprisingly called ConstraintKinds
instead of ConstraintKind
). Furthermore, we want to make use of type families. All in all, this leads to the following LANGUAGE
pragma:
{-# LANGUAGE ConstraintKinds, TypeFamilies #-}
We will define our own version of the Monad
class. Therefore, we have to hide the Monad
class from the Prelude:
import Prelude hiding (Monad (..))
We will need the module Data.Set
from the containers
package for some example code:
import Data.Set
Last, but not least, we have to import the kind Constraint
:
import GHC.Exts (Constraint)
Originally, classes and contexts were not first-class citizens in Haskell. The introduction of the Constraint
kind has changed this. Classes and contexts can now be used as parameters of types, for example. This is because they are now types themselves.
However, classes and contexts are still not types in the strict sense. There are still no values of type Eq
or Eq Integer
, for example. As I have explained in my previous post, Haskell’s notion of type is more general than the usual one. In particular, functions on types are types themselves. However, they are not types of kind *
. The same holds for classes and contexts. They are not types of kind *
, but they are types of some other kinds, so that they can generally be used in places where types can be used.
The new kind Constraint
, which is exported by GHC.Exts
, is the kind of all contexts. Classes and contexts are now handled as follows:
Each class with parameters of kinds k_1
through k_n
is a type of kind k_1 -> k_n -> Constraint
.
Each tuple type (t_1, ..., t_n)
where t_1
through t_n
are of kind Constraint
is also of kind Constraint
and denotes the conjunction of t_1
through t_n
. As a corner case, the nullary tuple type ()
is also of type Constraint
and denotes the constraint that is always true.
A context can be any type of kind Constraint
.
These rules guarantee that classes and contexts can be used as before. For example, (Read val, Show val)
is still a context, because Read
and Show
are types of kind * -> Constraint
, so Read val
and Show val
are types of kind Constraint
, and therefore (Read val, Show val)
is a type of kind Constraint
.
However, classes and constraints can be used in new ways now. Here are some examples:
Classes can be partially applied, and the results can be used like classes again.
Classes, partially applied classes, and contexts can be parameters of types and instances of classes.
Aliases of classes, partially applied classes, and contexts can be defined using type
declarations.
Families of classes, partially applied classes, and contexts can be defined using type synonym families.
In the remainder of this article, I will illustrate the last two of these points.
Sometimes, the same conjunction of several contexts appears in multiple types. In such cases, it can become cumbersome to always write these conjunctions explicitly. For example, there might be several functions in a library that deal with values that can be turned into strings and generated from strings. In this case, the types of these functions will typically have a context that contains constraints Show val
and Read val
. With the Constraint
kind, we can define context aliases Text val
as follows:
type Text val = (Show val, Read val)
Instead of Show val, Read val
, we can now simply write Text val
in contexts.
A few years ago, there was an attempt to implement support for context aliases (often called class aliases) in GHC. With the Constraint
kind, this is now obsolete, as context aliases are now just a special kind of type aliases.
We will illustrate the use of context families by defining a generalized version of the Monad
class.
The actual definition of a monad from category theory says that a monad on a category 𝒞 consists of an endofunctor on 𝒞 and some natural transformations. In Haskell, however, a monad is defined to be an instance of the Monad
class, which contains the two methods return
and (>>=)
. Haskell monads are monads on the category Hask, the category of kind-*
types and functions.
There are monads in the category theory sense that are almost monads in the Haskell sense, but not quite. One example is the monad behind Haskell’s Set
type. There are reasonable implementations of return
and (>>=)
for Set
:
setReturn :: el -> Set el
setReturn = singleton
setBind :: (Ord el, Ord el') =>
Set el -> (el -> Set el') -> Set el'
setBind set1 gen2 = unions (Prelude.map gen2 (toList set1))
The problem is that the type of setBind
is too restrictive, as it restricts the choice of element types by a context, whereas there is no such restriction in the type of (>>=)
. The reason for the restriction on element types is that the Set
monad is not a monad on the category Hask, but on the full subcategory of Hask whose objects are the instances of Ord
.
Using context families, we can generalize the Monad
class such that restrictions on the type parameters of Monad
instances become possible. We introduce a type synonym family Object
such that Object mon val
is the constraint that the parameter val
must fulfill when working with the Monad
instance mon
. We provide a default definition for Object
that does not restrict monad parameters. Finally, we change the types of return
and (>>=)
such that they restrict their Monad
instance parameters accordingly. The new declaration of the Monad
class is as follows:
class Monad mon where
type Object mon val :: Constraint
type Object mon val = ()
return :: Object mon val =>
val -> mon val
(>>=) :: (Object mon val, Object mon val') =>
mon val -> (val -> mon val') -> mon val'
We can now make Set
an instance of Monad
:
instance Monad Set where
type Object Set el = Ord el
return = setReturn
(>>=) = setBind
We can make every instance of the original Monad
class an instance of our new Monad
class. Because of the default definition of Object
, we do not need to define Object
in these cases. So the instance
declarations can look exactly like those for the original Monad
class. Here is an example for the []
type:
instance Monad [] where
return x = [x]
(>>=) = flip concatMap
In this article, I will present several of Haskell’s type system features. Some of them belong to the standard, others are only available as extensions. This is a write-up of a talk I gave on 31 January 2013 during the Theory Lunch of the Institute of Cybernetics. This talk provided the basics for another Theory Lunch talk, which was about the Constraint
kind.
This whole article was written as a literate Haskell file with ordinary text written in Markdown. You can download this literate Haskell file, read it, and load it into GHCi to play with the code. The HTML for the blog post was generated using Pandoc.
We first enable some language extensions that we will use in this article:
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies #-}
We will reimplement some bits of the Prelude for illustration purposes, and we will use functions from other modules whose names clash with those of certain Prelude functions. Therefore, we have to hide parts of the Prelude:
import Prelude hiding (Eq (..), Functor (..), lookup, (!!))
Furthermore, we need some additional modules for example code:
import Data.Char
import Data.Natural
import Data.Stream hiding (map)
import Data.Set hiding (map)
import Data.IntSet hiding (map)
These imports require the packages Stream
, natural-numbers
, and containers
to be installed.
Types typically denote sets of values. For example, Integer
denotes the set of all integers, Char
denotes the set of all characters, and [Bool]
denotes the set of all truth value lists.
However, Haskell uses a more general notion of type, which also covers functions on types. So for example, the unapplied list type constructor []
is also considered a type, as is a partial application of the function type constructor like (->) Integer
. Clearly, these types do not contain values.
To distinguish between ordinary types and functions on types, Haskell uses a system of kinds. Kinds are the “types of types”, so to say. A kind is either *
or has the form kind1 -> kind2
, where kind1
and kind2
are again kinds. The kind *
is the kind of all ordinary types (that is, types that contain values), and a kind1 -> kind2
is the kind of all type-level functions from kind1
to kind2
. Following are some examples of types and their kinds:
Integer, Char, [Bool], [[Bool]], [val] :: *
[], Maybe :: * -> *
Note that in a kind kind1 -> kind2
, kind1
and kind2
can be kinds of function types again. So higher-order types are possible. As a result, type functions with several arguments can be implemented by means of Currying. For example, Haskell’s pair type and function type constructors are kinded as follows:
(,), (->) :: * -> * -> *
Furthermore, we can have type constructors with type function arguments. Take the following generic types of trees and forests as an example:
data Tree func label = Tree label (Forest func label)
newtype Forest func label = Forest (func (Tree func label))
From these types, we can get various more concrete types by partial application:
type RoseTree = Tree []
type RoseForest = Forest []
type NonEmptyList = Tree Maybe
type PossiblyEmptyList = Forest Maybe
type InfiniteList = Tree Identity
The Identity
type used in the definition of Stream
is defined as follows:
newtype Identity val = Identity val
I also want to mention that if we have a type Event
in functional reactive programming, Tree Event
is the type of behaviors that change only at discrete times, and Forest Event
is the type of event streams.
A type class denotes a set of types, which are called the instances of the class. Each class declares a set of methods that its instances have to implement.
As an example of a type class, let us partially reimplement the Eq
class from the Prelude, whose methods are (==)
and (/=)
:
class Eq val where
(==), (/=) :: val -> val -> Bool
Eq
is supposed to cover all types whose values can be checked for equality. Here is an instance
declaration for the Bool
type:
instance Eq Bool where
False == bool2 = not bool2
True == bool2 = bool2
bool1 /= bool2 = not (bool1 == bool2)
It is possible to define classes whose instances have a kind other than *
. These are sometimes called constructor classes. An example of such a class is the Functor
class from the Prelude, whose instances have kind * -> *
. Here is a reimplementation:
class Functor func where
fmap :: (val -> val') -> (func val -> func val')
Typical instances of Functor
are []
and Maybe
:
instance Functor [] where
fmap = map
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap fun (Just val) = Just (fun val)
Types Tree func
and Forest func
can also be made instances of Functor
, provided that func
is a Functor
instance itself.
instance Functor func => Functor (Tree func) where
fmap fun (Tree root subtrees) = Tree (fun root)
(fmap fun subtrees)
instance Functor func => Functor (Forest func) where
fmap fun (Forest trees) = Forest (fmap (fmap fun) trees)
Note that these instance
declarations make the specialized versions of Tree
and Forest
that we have defined above automatically instances of Functor
.
GHC allows classes to have multiple parameters. While single-parameter classes denote sets of types, multi-parameter classes denote relations between types. An example of a class with two parameters is the class that relates types for which there is a conversion function:
class Convertible val val' where
convert :: val -> val'
We can convert from type Int
to type Integer
, but also between types Int
and Char
:
instance Convertible Int Integer where
convert = toInteger
instance Convertible Int Char where
convert = chr
instance Convertible Char Int where
convert = ord
Haskell allows us to define new types using data
declarations. An example of such a declaration is the following one, which introduces a type of lists:
data List el = Nil | Cons el (List el)
Furthermore, we can use type
declarations for defining aliases of existing types. For example, we can use the following type
declaration to define types of functions whose domain and codomain are the same:
type Endo val = val -> val
Both data
and type
declarations have in common that the types they define are parametric. Informally speaking, this means that the basic structure of the defined types is independent of type parameters. For example, lists are always either empty or pairs of an element and another list, no matter what the element type is. The choice of an el
parameter only determines the structure of elements. Likewise, values of a type Endo val
are always functions whose domain and codomain are the same. The val
parameter just determines the concrete domain and codomain type in use.
There are situations, however, where we want to define type-level functions that yield completely differently structured types for different arguments. This is possible with the type family extension that GHC provides.
There exist two flavors of type families: data families and type synonym families. Data families introduce new types and use the data
keyword, while type synonym families define aliases for types and use the type
keyword. This is analogous to data
and type
declarations, respectively. Type families can be stand-alone or associated. The former variant is analogous to top-level functions, while the latter is analogous to class methods. We will only deal with the latter in this post.
As an example of a data family, we define a type of total maps, that is, maps that assign values to every value of a chosen key type. Essential to our definition is that different key types lead to differently structured maps. We declare a class of key types, which contains the data family for total maps:
class Key key where
data TotalMap key :: * -> *
lookup :: key -> TotalMap key val -> val
Let us now give an instance
declaration for Bool
. Total maps with boolean keys are essentially pairs of values, consisting of one value for the False
key and one value for the True
key. Our instance
declaration reflects this:
instance Key Bool where
data TotalMap Bool val = BoolMap val val
lookup False (BoolMap falseVal _) = falseVal
lookup True (BoolMap _ trueVal) = trueVal
Total maps whose keys are natural numbers correspond to infinite lists, that is, streams of values:
instance Key Natural where
data TotalMap Natural val = NaturalMap (Stream val)
lookup nat (NaturalMap str) = str !! fromIntegral nat
More advanced things are possible. For example, pairs of keys can again serve as keys. A total map of a type TotalMap (key1,key2) val
corresponds to a function of type (key1,key2) -> val
, which in turn corresponds to a function of type key1 -> key2 -> val
. This suggests how to implement total maps with pair keys:
instance (Key key1, Key key2) => Key (key1,key2) where
data TotalMap (key1,key2) val
= PairMap (TotalMap key1 (TotalMap key2 val))
lookup (key1,key2) (PairMap curriedMap)
= lookup key2 (lookup key1 curriedMap)
Let us now look at an example of a type synonym family. We define a class of collection types where a collection is basically anything that contains elements. Here are two examples of collection types:
Set el
for any type el
that is an instance of Ord
IntSet
Our class contains a type synonym family that tells for every collection type what the corresponding type of collection elements is. The class
declaration is as follows:
class Collection coll where
type Element coll :: *
member :: Element coll -> coll -> Bool
Now we can form instance
declarations for the above example collection types:
instance Ord el => Collection (Set el) where
type Element (Set el) = el
member = Data.Set.member
instance Collection IntSet where
type Element IntSet = Int
member = Data.IntSet.member
The paper covers two topics:
Happy reading!
Here is my personal BibTeX entry for the paper:
@article{jeltsch:entcs-286, author = {Jeltsch, Wolfgang}, title = {Towards a Common Categorical Semantics for Linear-Time Temporal Logic and Functional Reactive Programming}, journal = {Electronic Notes in Theoretical Computer Science}, volume = 286, pages = {229--242}, numpages = 14, publisher = {Elsevier}, address = {Amsterdam, The Netherlands}, month = sep, year = 2012, issn = {1571-0661}, doi = {10.1016/j.entcs.2012.08.015} }
I am always happy to receive bug reports, wishlists, and contributions. Just get in touch with me.