summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdoardo La Greca2025-04-01 02:14:18 +0200
committerEdoardo La Greca2025-04-01 02:14:18 +0200
commitfad78ca8978b8b921d0706b967769708d407c0a1 (patch)
tree2274f304de0cbb81b28ae7027e7a99405c6824a9
parentbe1534d2c1c9d8f7712a59203f1c931d0d026813 (diff)
finish first lecture's notes and rephrase some old paragraphs
-rw-r--r--lec01/notes.md63
1 files changed, 47 insertions, 16 deletions
diff --git a/lec01/notes.md b/lec01/notes.md
index 87b1f8c..40fa874 100644
--- a/lec01/notes.md
+++ b/lec01/notes.md
@@ -76,9 +76,9 @@ One neat feature is that new infix operators can be implemented using just funct
One downside, however, is that negative numbers need to be wrapped between parentheses, since their "minus" is also an operator and Haskell has no operator precedence.
-Another thing to note is that Haskell does not implicitly convert between types. If we have an integer value and our function expects any other type, we can use the `fromIntegral` function. Similarly, if we have a floating point value and we need an integer, there are functions such as `round`, `floor`, and `ceiling`, which have the same meaning as in math. Although this may sound annoying, it's actually useful as implicit conversions are a source of errors.
+Another thing to note is that Haskell does not implicitly convert between types. If you have an integer value and our function expects any other type, you can use the `fromIntegral` function. Similarly, if you have a floating point value and you need an integer, there are functions such as `round`, `floor`, and `ceiling`, which have the same meaning as in math. Although this may sound annoying, it's actually useful as implicit conversions are a source of errors.
-Another little downside is that the slash symbol (`/`) only performs floating point division and for an integer division we must use the `div` function, optionally as an infix operator.
+Another little downside is that the slash symbol (`/`) only performs floating point division and for an integer division you must use the `div` function, optionally as an infix operator.
#### Boolean algebra
@@ -90,26 +90,32 @@ Haskell provides `if` expressions as part of its syntax. They are written as: `i
### Function definitions
-Functions can be written by cases.
+The following is the most basic way of writing functions.
sumtorial :: Integer -> Integer
sumtorial 0 = 0
sumtorial n = n + sumtorial (n-1)
-First of all, let's look at the type of `sumtorial`: `Integer -> Integer`. It means that the function takes an integer as input an it outputs another integer.
+The first line in a function definition typically defines the types of its arguments and return value. In this case, the function accepts one argument of type `Integer` and returns a value of type `Integer`. If the function accepted two arguments instead of one, its type would have been `Integer -> Integer -> Integer`.
-Secondly, we call *clauses* what's between the function name and the equal sign (`=`). They can be any valid pattern, including constants. Every time a function like this is called with a value, that value is compared to each clause until it either matches one of the provided constants or ends up in a variable name. Patterns are checked in order, starting from the top clause and going down to the bottom. After the first matching clause is found, it is chosen for the expression evaluation. This is important for recursive functions: the base cases always go on top, otherwise we may create an infinite loop.
+Following the function's type definition is a list of cases to match the function's arguments against. In fact, functions in Haskell are written by cases and each case contains a *clause*. Clauses are evaluated in order, from top to bottom, and may contain constant values as well as valid patterns for **pattern matching**. You write clauses between the function name, which is the first word on the line, and the equal sign (`=`). After the first matching clause is found, it is chosen and its expression evaluated.
-There is another way of writing functions, using **guards**:
+The expression, which is the part after the equal sign (`=`), usually contains variables defined in the pattern, which are replaced with the argument's value during the function call. However, it's possible to reference external variables or to just put a constant as expression.
+
+For recursive functions, like the example we just analyzed, it's important to always place the base cases on top, unless you want to create an infinite loop.
+
+There is another way of writing functions, using **guards**.
hailstone :: Integer -> Integer
hailstone n
| n `mod` 2 == 0 = n `div` 2
| otherwise = 3*n + 1
-In this example, `n` is our only clause and it has two *guards*.
+In this example, `n` is our only clause and it has two guards: one checks whether `n` is a factor of two, while the other matches all remaining values using the `otherwise` keyword.
-In guards, after entering a clause due to its matching pattern, conditions are evaluated from top to bottom and the first which evaluates to `True` is chosen. `otherwise` is matched when none of the above evaluated to `True`, `otherwise` always evaluates to `True`. Each clause of a function definiton can be associated with any number of guards, but each guard must evaluate to a Boolean expression. Boolean expressions in guards can evaluate to a constant which does not depend on any of the function's arguments.
+In guards, after entering a clause with matching pattern, conditions are evaluated from top to bottom and the first that evaluates to `True` is chosen. The special keyword `otherwise` is reached when none of the above evaluated to `True` and it always evaluates to `True`. In fact, `True` is the actual value of the `otherwise` keyword.
+
+There can be any number of guards for each clause, but each guard must evaluate to a Boolean expression. Boolean expressions may not depend on any of the function's arguments and just be constant expressions. In case none of the guards provided for a clause is matched, the next clause is considered.
### Pairs
@@ -120,40 +126,65 @@ Pairs are simply pairs of values.
Values are not required to have the same type, as the example shows. The notation with parentheses and a comma is used for both the type and the value of a pair.
-Pairs are valid patterns for clauses.
+Pairs are valid patterns for pattern matching in clauses and the variable names provided between parentheses take the value of the pair's elements.
sumPair :: (Int,Int) -> Int
sumPair (x,y) = x + y
### Functions with many arguments
-Functions can have more than one argument. In these cases, the function type grows with the additional types separated by arrows while the clauses can simply add variables to patterns.
+Functions can have more than one argument. In such cases, the function type grows while the clauses can simply add variables to patterns.
f :: Int -> Int -> Int -> Int
f x y z = x + y + z
-Since function application has higher precedence over any infix operator, infix operations whose result is passed as function argument must be wrapped in parentheses.
+Since function application has higher precedence over infix operators, infix operations whose result is passed as function argument must be wrapped in parentheses.
f 3 (n+1) 7
### Lists
-Lists are a basic data type in Haskell. They are written as their type surrounded by square brackets.
+Lists are one of the types Haskell supports by default. They are written as their type surrounded by square brackets.
nums :: [Integer]
nums = [1,2,3,19]
Haskell provides **list comprehensions**.
-Strings are lists of characters, `String` is the same as `[Char]`, which use special syntax for their values: instead of writing lists of characters, they are simply written with double quotes. This is useful because it allows to process Strings just as any other list.
+Strings are lists of characters, `String` is the same as `[Char]` and it uses a special syntax for its values: instead of writing lists of characters, they are simply written with double quotes. This generalization allows Strings to be processed just like any other list.
-Lists can be empty. The empty list is the simplest list.
+Lists can be empty.
myList = []
-By using the **cons operator** (`:`), we can build lists on top of the empty list. The cons operator, provided an element and a list, produces a new list with that element as first elements, followed by all the other elements in the list. For convenience, we can use `[1, 2, 3]` instead of `1 : 2 : 3 : []`.
+By using the **cons operator** (`:`), you can create new lists from existing lists, including the empty list. In fact, the cons operator, provided an element and a list, respectively as first and second operands, produces a new list with that element as first, followed by all the other elements in the list. For convenience, you can use `[1, 2, 3]` instead of `1 : 2 : 3 : []`.
-It's important to note that *lists are not arrays*. Lists are, in fact, singly linked lists.
+It's important to note that *lists are not arrays*. Lists are, in fact, singly linked lists under the hood.
### Functions on lists
+The pattern matching syntax supports both lists and the cons operator. They are useful for working on lists in functions.
+
+ intListLength :: [Integer] -> Integer
+ intListLength [] = 0
+ intListLength (x:xs) = 1 + intListLength xs
+
+The function above calculates the length of a list. As you can see, the first clause matches the empty list and returns zero. It makes sense: an empty list has zero elements. The second clause matches the first element `x` in the list, followed by the rest of the list, named `xs`.
+
+The expression associated with the second clause may look tricky at a first look but it's actually really simple: the list of an element `x` followed by a list `xs` is 1 plus the length of the list `xs`. In other words, the length of a list of `n` elements is just 1 plus the length of the same list with the first element removed. The process of removing the first element of the list and recursively evaluating the list again is repeated until the remaining list has length zero. The result of the recursive expression is something like `1 + 1 + ... + 0`.
+
+Here, the variable `x` is not used at all. It's useless and it has no reason to exist. In such cases, variable names can be replaced with underscores. It's a special syntax to tell Haskell to not bother providing a name for that value.
+
+ intListLength (_:xs) = 1 + intListLength xs
+
+### Nested patterns
+
+Patterns *can* be nested. This allows you to write more powerful pattern matching expressions.
+
+For example, if, instead of just the first element of a list, you wanted to get the value of the first two, you can write `(x:(y:ys))` instead of `(x:xs)`. Actually, you don't need extra parentheses: `(x:y:ys)`.
+
+### Combining functions
+
+It's smart and elegant to build complex functions by combining simple ones. Thanks to Haskell's lazy evaluation, it is possible to compute and use memory just "as much as needed".
+
+In fact, a function that creates a list, does some computations on it, and then returns a value, only creates as many list elements as needed for the computation. This happens because every element of said list is generated as soon as it's used. The calculations made on the list do not start as soon as the list has been fully generated, but rather after the first element has been generated.