diff options
author | Edoardo La Greca | 2025-07-01 17:36:45 +0200 |
---|---|---|
committer | Edoardo La Greca | 2025-07-02 18:42:35 +0200 |
commit | 19175762c6025ec21ef836cc081711f8f9897034 (patch) | |
tree | 8d49b38207339284d591a22c84713c412d98c680 /lec03/notes.md | |
parent | 917e867b0f34213057ea52e765fba6d7fa55410e (diff) |
add notes for lecture 3
Diffstat (limited to 'lec03/notes.md')
-rw-r--r-- | lec03/notes.md | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/lec03/notes.md b/lec03/notes.md new file mode 100644 index 0000000..751ce63 --- /dev/null +++ b/lec03/notes.md @@ -0,0 +1,94 @@ +# Lecture 3: Recursion patterns, polymorphism, and the Prelude + +Although recursive functions can compute pretty much everything, Haskell programmers hardly write recursive functions. In fact, there exist some common patterns in recursive functions, which can be abstracted into library functions and made available to programmers, which avoids thinking about low-level details while writing Haskell. This is the goal of *wholemeal programming*. + +These patterns, and recursion in general, are very common when processing many values of the same type in the same way, typically stored in specialized structures that can hold many of them like arrays and lists (here generalized as "lists" only, for the sake of simplicity and readability). Of course that's not what recursion is only about, but here we'll focus on that. + +## Recursion patterns + +There are three very common recursion patterns: +- map +- filter +- fold + +### Map + +**Map** is a pattern that performs an operation on each element of a list and returns the resulting list. The resulting list has the same length as the initial list and the elements of these two lists follow the same order. + +In other words, maps take the first element of a list, apply an operation, then store the result in the first element of a new list and repeat this process on all the other elements, the result of which will go in the corresponding positions in the new list. Operations can return values of any type, not necessarily the same as the processed element. + +### Filter + +**Filter** is a pattern that checks whether a condition is true on each element of a list, keeping all those for which the condition is true and discarding the others. The elements' order is kept the same. + +### Fold + +This pattern "summarizes" the elements of a list, but it's going to be explained in the next lecture. + +## Polymorphism + +Polymorphism lets us generalize the type we're dealing with. It enables us to, for example, process lists of every type with one function, as opposed to writing a function for each possible type. + +### Polymorphic data types + +This below is a polymorphic data type. It defines a recursive List. + + data List t = E | C t (List t) + +Note that there is an additional parameter `t` after `data List`, it's the **type variable**! A type variable can stand for any type and must start with a lowercase letter. When a type has a type variable, it is said to be **parametrized by a type**. It's analogous to a function parameter in the context of functions. + +In our example, by giving `t` a defined type, the constructor `C` takes a value of that type, together with another `(List t)`. + + lst :: List Int + lst = C 3 (C 5 (C 2 E)) + +### Polymorphic functions + +Functions also support type variables! If we wanted to write a generic map function, we could write the following: + + mapList :: (t -> t) -> List t -> List t + +However, this has a huge limitation which Haskell's own `map` does not have: the resulting list must have the same type of elements as the list we started with. To handle the case of different lists, we can write: + + mapList :: (a -> b) -> List a -> List b + +It's important to remember that *the caller gets to pick the types*. When writing a polymorphic function, we need to ensure that it can work with any type. + +## The Prelude + +The **Prelude** is a standard module implicitly imported into any Haskell program and contains useful standard definitions. For example, it contains polymorphic lists and polymorphic functions for working with them. + +It also includes `Maybe`, the value of which either contains some value of some type or nothing at all: + + data Maybe a = Nothing | Just a + +## Total and partial functions + +**Partial functions** are functions which may crash or recurse infinitely if they hit an edge case. The opposite of a partial function is a **total function**. + +There are partial functions in the Prelude. `head`, `tail`, `init`, `last`, and `!!` are all partial functions and should not be used! It is good practice in Haskell to avoid partial functions as much as possible. + +There are several ways to avoid partial functions. Pattern-matching can sometimes help replacing partial functions in favor of other functions which are obviously total. + +However, sometimes we need to write a partial function. In such case, there are two approaches: +- Changing the output type of the function, so that it can indicate a possible failure (for example, using `Maybe`). +- Changing the input type of the function, so that it indicates that the value that triggers the edge case cannot exist. This is particularly useful in cases where there are a lot of functions which could, in theory, all fail on some value, but that value is assured to not exist while using them. + +As an example for the second approach, think of the `head` function. It crashes if the list is empty. If our program only works with non-empty lists, creating a new `head` function using `Maybe` can be very annoying due to the overuse of its constructors. + +Instead, we could simply create a type that represents non-empty lists, like `NonEmptyList` below. With it, not only we ensure that there is nothing unsafe in our program, but we do so by delegating our program's safety to the type system, and, therefore, it'll be up to the compiler to ensure that our program is safe. + + data NonEmptyList a = NEL a [a] + + nelToList :: NonEmptyList a -> [a] + nelToList (NEL x xs) = x:xs + + listToNel :: [a] -> Maybe (NonEmptyList a) + listToNel [] = Nothing + listToNel (x:xs) = Just $ NEL x xs + + headNEL :: NonEmptyList a -> a + headNEL (NEL a _) = a + + tailNEL :: NonEmptyList a -> [a] + tailNEL :: (NEL _ as) = as |