Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
pp:l04x [2020/03/19 21:19] pdmatei |
pp:l04x [2021/04/05 14:51] (current) alexandru.poboranu add a Context in the last 2 ex |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Algebraic Datatypes in Haskell ====== | + | ====== 5. Algebraic Datatypes in Haskell ====== |
- | ===== The type List ===== | + | There are multiple ways of defining new data types in Haskell. In this session, we will focus on //Algebraic Data Types//, which allow us to specify multiply constructors, with any number of arguments. |
- | 1. Define the type //list with elements of type ''Integer''//: | + | A new ADT can be defined using the keyword ''data'' followed by the desired name (necessarily capitalized!), an equal sign and then all constructors (capitalized!) separated by a pipe ''|'': |
- | <code haskell> | + | |
- | data IntList = ... | + | |
- | </code> | + | |
- | 2. Define a function which computes the sum of elements of such a list: | ||
<code haskell> | <code haskell> | ||
- | isum :: IntList -> Integer | + | data Ordering = LT | EQ | GT |
</code> | </code> | ||
- | 3. Define type **polymorfic** type ''List a'' encoding lists with elements of type ''a'': | + | This type is already defined in haskell and is used whenever comparisons are needed (its constructors stand for "less than", "equal" and "greater than"). Think of how the standard C function ''strcmp'' takes two strings and returns a value indicating their lexicographic order: |
- | <code haskell> | + | |
- | data List a = | + | |
- | </code> | + | |
- | 4. Define a function which converts ''IntList'' lists to ''List Integer'' lists: | + | * a negative number if the first string comes first |
- | <code haskell> | + | * 0 if they are equal |
- | to_poly_list :: IntList -> List Integer | + | * a positive number if the second string comes first |
- | </code> | + | |
- | 5. Define a function which displays lists. What type will this function have? | + | In Haskell, it would return an ''Ordering''. |
- | <code haskell> | + | |
- | show_list = ... | + | |
- | </code> | + | |
- | Add the tree datatype definition from the lecture: | + | 1. Write a function ''myCompare'' which takes two integers and returns an ''Ordering'' indicating their relationship (there is already a function in Haskell, ''compare'', which does this for all types of numbers). |
- | <code haskell> | + | |
- | data Tree a = Void | Node (Tree a) a (Tree a) deriving Show | + | |
- | </code> | + | |
- | 6. Implement the function ''flatten'': | + | 2. Write a function which takes an integer and a **sorted** (ascending) list of integers and inserts that number into the proper position so that the resulting list is still sorted. |
- | <code haskell> | + | |
- | flatten :: Tree a -> List a | + | <code> |
+ | insert 7 [1,5,22,86] -> [1,5,7,22,86] | ||
</code> | </code> | ||
- | 7. Define list concatenation over type ''List a'': | + | 3. Write a function ''insertSort'' that makes use of ''insert'' to implement [[https://en.wikipedia.org/wiki/Insertion_sort|insertion sort]] on a list. |
+ | |||
+ | We now have a sorting function. However, it is unsatisfactory, because it can only sort numbers in an ascending order. We cannot specify any other criteria (e.g. descending order, by number of prime factors, by number of digits). We will expand the functionality by employing the ''Ordering'' data type and an auxiliary "comparator function" which takes two integers and returns an ''Ordering''. | ||
+ | |||
+ | 4. Write a function ''insertBy'' which takes a comparator function, an integer and a sorted list of integers (s.t. $ \forall i, j; i < j, comparator(l_i, l_j) \neq GT$) and uses the comparator to insert the element at the proper location. | ||
+ | |||
+ | 5. Write a function ''insertSortBy'' which insert-sorts a list, with the aid of a comparator function (there is a function in the ''Data.List'' module named ''sortBy'' that does the same thing, though it uses another sorting algorithm). | ||
+ | |||
+ | A data type's constructors can also take arguments; when defining the data type, we need to specify the types of these arguments: | ||
<code haskell> | <code haskell> | ||
- | app :: (List a) -> (List a) -> (List a) | + | data Point = Point Float Float |
</code> | </code> | ||
- | 8. Define the function ''tmap'' which is the ''Tree a'' correspondent to ''map::(a->b) -> [a] -> [b]''. | + | Note that the constructor here can have the same name as the data type, because they are very different things and could never cause confusion (they can never appear in the same contexts). |
+ | |||
+ | Once defined this way, we can use regular **pattern matching** for our constructors: | ||
- | 9. Define the function ''tzipWith'': | ||
<code haskell> | <code haskell> | ||
- | tzipWith :: (a -> b -> c) -> (Tree a) -> (Tree b) -> (Tree c) | + | pointToPair :: Point -> (Float, Float) |
+ | pointToPair (Point x y) = (x, y) | ||
</code> | </code> | ||
- | 10. Define the function ''tfoldr'': | + | 6. Write a function ''distance'' which takes two points and computes their euclidian distance. |
+ | |||
+ | 7. Write a function ''collinear'' which takes three points and tells whether they are on the same line. (one way to do this is to compute the area of the triangle they define and make sure it's 0). | ||
+ | |||
+ | <note tip> | ||
+ | If you ask ''ghci'' to print a ''Point'' (e.g. by simply typing ''Point 2.0 3.1'' at the prompt) it will throw an error, as it doesn't know how to do that. You can use the following definition to obtain a "default" representation. | ||
<code haskell> | <code haskell> | ||
- | tfoldr :: (a -> b -> b) -> b -> Tree a -> b | + | data Point = Point Float Float deriving Show |
</code> | </code> | ||
- | Before implementing it, consider what should ''tfoldr (+) 0'' do, and how should this value be computed. | ||
- | 11. Implement the flattening function using ''tfoldr'': | + | The meaning of ''deriving Show'' will be discussed in a future session. |
+ | </note> | ||
+ | |||
+ | We can also define **recursive data types**, whose constructors can depend on another value of the same type. | ||
<code haskell> | <code haskell> | ||
- | tflatten = ... | + | data Natural = Zero | Succ Natural |
</code> | </code> | ||
- | Sometimes, instead of doing pattern matching in a function definition, e.g.: | + | Here, we say that a natural number is either the number zero, or the successor of another natural number (see [[https://en.wikipedia.org/wiki/Peano_axioms|Peano axioms]]). The number three would be ''Succ (Succ (Succ Zero)))''. |
+ | |||
+ | 8. Write a function ''add'' which takes two ''Natural'' numbers and returns their sum (remember that you can do pattern matching to check the structure of a number). | ||
+ | |||
+ | 9. Write a function ''mul'' which takes two ''Natural'' numbers and returns their product. | ||
+ | |||
+ | <note tip> | ||
+ | Again, it might be useful to display these data types so add ''deriving Show'' to the definition: | ||
<code haskell> | <code haskell> | ||
- | f [] = "Empty" | + | data Natural = Zero | Succ Natural deriving Show |
- | f (x:y:xs) = "At least two" | + | |
- | f _ = "Something else" | + | |
</code> | </code> | ||
+ | </note> | ||
+ | |||
+ | Using recursive data types, we can also define immutable lists of integers: | ||
- | it is useful to do this in the body of the function, or in another expression. This can be done using the ''case'' instruction. Example: | ||
<code haskell> | <code haskell> | ||
- | f l = | + | data IList = IVoid | ICons Int IList |
- | case l of | + | |
- | [] -> "Empty" | + | |
- | (x:y:xs) -> "At least two" | + | |
- | _ -> "Something else" | + | |
</code> | </code> | ||
- | 12. Consider the following definition of natural numbers extended with the value ''Infinity'': | + | However, we have seen that haskell lists are generic, capable of containing any type of element, not just integers. For this we need a **polymorphic data type** (as opposed to all the previous ones which are **monomorphic**). That is, a type that depends on another type. |
<code haskell> | <code haskell> | ||
- | data Extended = Infinity | Value Integer | + | data List a = Void | Cons a (List a) |
</code> | </code> | ||
- | Define a function which computes the sum of two ''Extended'' values: | + | In the above definition, we have mostly replaced ''Int'' with ''a''. ''a'' is a **type variable**; it can stand for any type (what makes it a type variable is that it starts with a lowercase letter; it could as well be ''b'' or ''something''). We can now construct lists of any type. However, remember that all elements in a list must be of the //same// type; we can't combine integers and strings. That is why ''a'' also follows the data type name, on the left hand side of the ''='': if we have a ''List'' which contains ''Int''s, then that is a ''List Int''. |
+ | |||
+ | 10. Write a function ''myLength'' which takes a ''List a'' and returns its length. Notice how the type of the lists elements doesn't matter (and remember pattern matching!). | ||
+ | |||
+ | 11. Write a function ''toHaskell'' which converts our lists to haskell's lists. That is, it takes a ''List a'' and returns a ''[a]''. | ||
+ | |||
+ | 12. Define a **recursive**, **polymorphic** data type to represent binary trees. A binary tree is either the empty tree, or a node containing a value, a left subtree and a right subtree. | ||
+ | |||
+ | 13. Write a function ''height'' which takes a binary tree and returns its height. | ||
+ | |||
+ | 14. Write a function ''size'' which takes a binary tree and returns the number of nodes. | ||
+ | |||
+ | 15. Write a function ''mirror'' which takes a tree and mirrors it (for each node, the left subtree becomes the right subtree and vice-versa). | ||
+ | |||
+ | 16. Write a function ''treeMap'' which takes a function ''a -> b'' and a tree with values of type ''a'' and applies the function on each of the tree's elements to yield a tree with values of type ''b'' (exactly like ''map'' for lists). | ||
+ | |||
+ | 17. Write a function ''flatten'' which takes a binary tree and collects all its values into a list (note that there are multiple ways to go through all the values, such as [[https://en.wikipedia.org/wiki/Tree_traversal|preorder, inorder, postorder]]; the choice is up to you). | ||
+ | |||
+ | Consider the following data type which defines a ''Student'', charactarized by a first name, a last name and a list of grades: | ||
<code haskell> | <code haskell> | ||
- | extSum :: Extended -> Extended -> Extended | + | data Student = Student String String [Float] |
- | extSum v v' = | + | |
</code> | </code> | ||
- | (Question: can you implement it with a single ''case'' expression?) | ||
- | 13. Define a function which computes the equality of two ''Extended'' values: | + | 18. Write a function ''avg'' to compute the average of a student's grades. |
+ | |||
+ | <note tip> | ||
+ | If you use ''length'' on a list and want to divide to it, you'll notice haskell raises an error when asked to divide a ''Float'' to an ''Int''; you can call ''fromIntegral'' on an ''Int'' to perform an explicit conversion. | ||
+ | </note> | ||
+ | |||
+ | 19. Write a function ''studComp'' which takes two students and returns an ''Ordering'' based on their averages. | ||
+ | |||
+ | 20. Write a function ''highestAverage'' which takes a list of students and returns the name and surname (separated by a space) of the student with the highest average. | ||
+ | |||
+ | |||
+ | The following data type models simple arithmetic expressions, where we can have variables, integer constants and addition and multiplication: | ||
<code haskell> | <code haskell> | ||
- | equal :: Extended -> Extended -> Bool | + | data AExpr = Const Int | Var String | Add AExpr AExpr | Mul AExpr AExpr |
</code> | </code> | ||
- | The polymorphic datatype: | + | Similarly, the following models simple boolean expressions that are either an equality/greather-than comparison between two arithmetic expressions or a negation of a boolean expressions: |
<code haskell> | <code haskell> | ||
- | data Maybe a = Nothing | Just a | + | data BExpr = Eq AExpr AExpr | Not BExpr | Gt AExpr AExpr |
</code> | </code> | ||
- | which is part of the Haskell default library is useful for error handling. For instance, if a computation fails, a function may return ''Nothing''. If the computation returns a result ''x'' of type ''a'', then the function will return a value ''Just x''. | ||
- | 14. Implement the function ''lhead'' which behaves like ''head'' but returns ''Nothing'' if the argument of the function is the empty list: | + | Our goal is to be able to evaluate an expression thus represented. First, we need a way to deal with variables: for this we define a context, which is a collection of mappings from a variable name to a value: |
<code haskell> | <code haskell> | ||
- | lhead :: List a -> Maybe a | + | type Context = [(String, Int)] |
</code> | </code> | ||
- | 15. Implement the function ''ltail'' | + | <note> |
+ | Note that we used the more lightweight ''type'' to obtain a **type synonym**, because we don't actually need the power of ADTs. | ||
+ | </note> | ||
+ | |||
+ | 21. Write a function ''search'' which takes a ''Context'' and a variable name and returns the value from that context (it is guaranteed, that exactly one value exists for each variable on which the function is called; you don't need to account for any edge-case). | ||
+ | |||
+ | 22. Write a function ''evalA'' which takes a ''Context'' and an ''AExpr'' and returns a single integer representing its evaluated value. | ||
+ | |||
+ | 23. Write a function ''evalB'' which takes a ''Context'' and a ''BExpr'' and returns a single boolean representing its evaluated value. |