===== Abstract Datatypes ===== ==== Intro ==== An Abstract Datatype relies on //functions// to describe the possible values of a type. We start with a simplistic example: data Nat = Zero | Succ Nat * the expression ''data Nat'' introduces a new **type** in the programming language * after ''='', the **base constructors** of the type follow. In Haskell, **all constructors** must begin with a capital letter * ''Zero'' is a nullary constructor. * ''Succ Nat'' designates an **internal** constructor, which expects a natural number (of type ''Nat''). A value ''(Succ x)'' **is** of type ''Nat'' (i.e. a natural number), and we may be tempted to see it as a function call, which //returns// the successor of ''x'' ''Zero'' and ''Succ'' are called **data constructors** in Haskell. ''Nat'' is called a ''type'' or ''type-constructor''. We shall distinguish between the two in a later lecture. Note that the //internal// representation of an ADT, as perceived by the programmer, is //abstract//. We may see values as //calls// of special functions - data constructors. Except for their meaning (and language-level implementation), data constructors behave exactly as functions. For instance: * ''Zero :: Nat'' * ''Succ :: Nat -> Nat'' We continue the example with addition: add :: Nat -> Nat -> Nat add Zero y = y add (Succ x) y = Succ (add x y) An important observation is that the **pattern matching mechanism** in Haskell relies on data constructors, and their applications. For instance, the following definition is a correct usage of the pattern matching mechanism: f (1:y:[]) = ... I uses the data constructors ''(:)'' and ''[]'' for lists, as well as the data constructor ''1'' for integers. The pattern describes any list of integers which starts with a ''1'' and contains exactly two elements. ==== Monomorphic List implementation ==== In what follows, we give an implementation for the type //List of integers//. This type is called //monomorphic//, since our list can only contain elements of a single type (integer): data IList = Void | Cons Integer IList app :: IList -> IList -> IList app Void l = l app (Cons h t) l = Cons h (app t l) convert :: IList -> [Integer] convert Void = [] convert (Cons h t) = h : (convert t) mfoldl :: (b -> Integer -> b) -> b -> IList -> b mfoldl op acc Void = acc mfoldl op acc (Cons h t) = mfoldl op (op acc h) t mfoldr :: (Integer -> a -> a) -> a -> IList -> a mfoldr op acc Void = acc mfoldr op acc (Cons h t) = op h (mfoldr op acc t) convert2 :: IList -> [Integer] convert2 = mfoldr (:) [] showl :: IList -> String showl = show . convert2 In the above code, we have defined some basic list operations, such as ''app'' (list concatenation) and ''convert'', which transforms a list of type ''IList'' to a conventional Haskell list (of type ''[Integer]''). We have also implemented the two folding procedures for ''IList''. Note the type signature of each fold. Finally, we have used folds to provide an alternative implementation for convert, as well as for converting lists to strings. ==== Propositional Logic in Haskell ==== Abstract Datatypes are a natural way to define more elaborate data-structures, such as propositional formulae. We give a possible definition below: data Formula = Var String | And Formula Formula | Or Formula Formula | Not Formula We observe that: * ''Var :: String -> Formula'' * ''And :: Formula -> Formula -> Formula'' * ''Or :: Formula -> Formula -> Formula'' * ''Not :: Formula -> Formula'' A good exercise consists in the implementation of a display function for formulae: fshow :: Formula -> String fshow (Var v) = v fshow (And f1 f2) = "("++(fshow f1)++" ^ "++(fshow f2)++")" fshow (Or f1 f2) = "("++(fshow f1)++" V "++(fshow f2)++")" fshow (Not f) = "~("++(fshow f)++")" Next, we implement a function ''push'', which pushes negation //inward//: push :: Formula -> Formula push (Var v) = (Var v) push (Not (Var v)) = Not (Var v) push (Not (And f1 f2)) = Or (push (Not f1)) (push (Not f2)) push (Not (Or f1 f2)) = And (push (Not f1)) (push (Not f2)) push (And f1 f2) = And (push f1) (push f2) push (Or f1 f2) = Or (push f1) (push f2) Notice, at lines 4,5 the implementation of deMorgan's laws. Finally, we implement a function which computes the truth-value of a formula, under a certain interpretation. First, we define the type ''Interpretation'': type Interpretation = String -> Bool i :: Interpretation i "x" = True i "y" = False i "z" = True In the first line, we have defined a **type-alias**: ''Interpretation'' is a function from strings to booleans. We have also implemented a three-variable interpretation, for testing purposes. Next, we implement ''eval'': eval :: Interpretation -> Formula -> Bool eval i (Var v) = i v eval i (Not f) = not(eval i f) eval i (And f1 f2) = (eval i f1) && (eval i f2) eval i (Or f1 f2) = (eval i f1) || (eval i f2) ==== Monomorphic Trees in Haskell ==== data ITree = Leaf | Node ITree Integer ITree Note that: * ''Leaf :: ITree'' * ''Node :: ITree -> Integer -> ITree -> ITree'' Next, we implement a folding operation on Trees. The key to it is to conceptually define what folding should do on Trees. To grasp an intuition, recall that: * ''foldr (:) []'' is the **identity function** on lists. Hence, ''foldr (:) [] [1,2,3]'' produces ''[1,2,3]''. * also, recall that the map operation can be defined as: ''\f -> foldr ((:).f) []'' Similar to the list case, a fold on trees should: * **preserve the tree structure**, given the appropriate operator (e.g. ''Node''). * hence, the call ''mtfold Node Leaf'' (where ''Leaf'' is the accumulator) should be the **identity function on trees**. Let us define the identity function ''tid'' for trees: tid Leaf = Leaf tid (Node r k l) = Node (tid r) k (tid l) As already said, ''(tid t)'' is equivalent to the call ''(mtfold Node Leaf t)'', for any arbitrary tree ''t''. To obtain the general ''mtfold'' implementation, we simply generalise ''Node'' by an arbitrary operation ''op'': * if ''Node :: ITree -> Integer -> ITree -> ITree'', then * ''op :: b -> Integer -> b -> b'' We also generalise ''Leaf'' by an arbitrary accumulator: * if ''Leaf :: ITree'' then * ''acc :: b'' The code generalisation becomes: mtfold :: (b -> Integer -> b -> b) -> b -> ITree -> b mtfold op acc = let f (Node left key right) = f (f left) key (f right) f Leaf = acc in f Notice that ''f'' is precisely the generalisation of ''tid'' according to our observations. We can use ''mtfold'' to implement various tree operations such as: tsum = mtfold (\k r l->k + r + l) 0 tmirror = mtfold (\k r l -> Node l k r) Leaf tflatten = mtfold (\k r l -> r ++ [k] ++ l) []