1
1
mirror of https://github.com/thma/WhyHaskellMatters.git synced 2024-09-11 13:46:11 +03:00

declarative programming

This commit is contained in:
Mahler, Thomas 2020-04-01 18:42:45 +02:00
parent c7ad772c88
commit e829eaa4b3
2 changed files with 171 additions and 65 deletions

193
README.md
View File

@ -28,6 +28,7 @@ Nevertheless this article is not meant to be a full introduction to the language
- [Pattern matching (part 1)](#pattern-matching-part-1)
- [Algebraic Data Types](#algebraic-data-types)
- [Polymorphic Data Types](#polymorphic-data-types)
- [Declarative programming](#declarative-programming)
## Introduction
@ -524,7 +525,7 @@ or some element (of a data type `a`) followed by a list with elements of type `a
This intuition is reflected in the following data type definition:
```haskell
data [a] = [] | a : [a] deriving (Eq, Ord)
data [a] = [] | a : [a]
```
The cons operator `(:)` (which is an infix operator like `(.)` from the previous section) is declared as a
@ -542,8 +543,14 @@ A list containing the three numbers 1, 2, 3 is constructed like this:
1 : 2 : 3 : []
```
Luckily the language designers have been so kind to offer some syntactic sugar for this. So the first list can be
written as `[1]` and the second as `[1, 2, 3]`.
Luckily the Haskell language designers have been so kind to offer some syntactic sugar for this.
So the first list can simply be written as `[1]` and the second as `[1,2,3]`.
Polymorphic type expressions describe *families of types*.
For example, `(forall a)[a]` is the family of types consisting of,
for every type `a`, the type of lists of `a`.
Lists of integers (e.g. `[1,2,3]`), lists of characters (`['a','b','c']`),
even lists of lists of integers, etc., are all members of this family.
Function that work on lists can use pattern matching to select behaviour for the `[]` and the `a:[a]` case.
@ -560,65 +567,165 @@ and the length of a list whose first element is x and remainder is xs
is 1 plus the length of xs.
With this knowledge we can define a sample list of Integers:
```haskell
In our next example we want to work with a of some random integers:
```haskell
someNumbers :: [Integer]
someNumbers = [49,64,97,54,19,90,934,22,215,6,68,325,720,8082,1,33,31]
```
Function that work on lists will use the recursive type definition for pattern matching.
For example, the function `head` will return the first element of a list.
```haskell
-- | Extract the first element of a list, which must be non-empty.
head :: [a] -> a
head (x:_) = x
head [] = error "head: empty list"
-- | Extract the elements after the head of a list, which must be non-empty.
tail :: [a] -> [a]
tail (_:xs) = xs
tail [] = error "tail: empty list"
```
Now we want to select all even or all odd numbers from this list.
We are looking for a function `filter` that takes two
arguments: first a predicate function that will be used to check each element
and second the actual list of elements. The function will return a list with all matching elements.
And of course our solution should work not only for Integers but for any other types as well.
Here is the type signature of such a filter function:
```haskell
filter :: (a -> Bool) -> [a] -> [a]
filter _pred [] = []
```
In the implementation we will use pattern matching to provide different behaviour for the `[]` and the `(x:xs)` case:
```haskell
filter :: (a -> Bool) -> [a] -> [a]
filter pred [] = []
filter pred (x:xs)
| pred x = x : filter pred xs
| otherwise = filter pred xs
```
I think the `[]` case is obvious.
To understand the `(x:xs)` case we have to know that in addition to simple matching of the type constructors
we can also use *pattern guards* to perform additional testing on the input data.
In this case we compute `pred x` if it evaluates to `True`, `x` is a match and will be cons'ed with the result of
`filter pred xs`.
If it does not evaluate to `True`,
we will not add `x` to the result list and thus simply call filter recursively on the remainder of the list.
Let's start by defining a list containing some Integer numbers:
The type signature in the first line declares `someNumbers` as a list of Integers. The brackets `[` and `]` around the type `Integer`
denote the list type.
In the second line we define the actual list value. Again the square brackets are used to form the list.
The bracket notation is syntactic sugar the actual list construction based on an empty list `[]` and the
concatenation operator `(:)` (which is an infix operator like `(.)` from the previous section).
For example, `[1,2,3]` is syntactic sugar for `1 : 2 : 3 : []`.
The concatenation operator `(:)`
There is a nice feature called *arithmetic sequences* which allows you to create sequences of numbers quite easily:
Now we can use `filter` to select elements from our sample list:
```haskell
upToHundred :: [Integer]
upToHundred = [1..100]
someEvenNumbers :: [Integer]
someEvenNumbers = filter even someNumbers
oddsUpToHundred :: [Integer]
oddsUpToHundred = [1,3..100]
-- predicates may also be lambda-expresssions
someOddNumbers :: [Integer]
someOddNumbers = filter (\n -> n `rem` 2 /= 0) someNumbers
```
Of course we don't have to invent functions like `filter` on our own but can rely on the [extensive set of
predefined functions working on lists](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-List.html)
in the Haskell base library.
## Declarative programming
In this section I want to explain how programming with *higher order* functions can be used to
factor out many basic control structures and algorithms from the user code.
This will result in a more *declarative programming* style where the developer can simply
declare *what* she wants to achieve but is not required to write down *how* it is to be achieved.
Code that applies this style will be much denser and it will be more concerned with the actual elements
of the problem domain than with the technical implementation details.
We'll demonstrate this with some examples working on lists.
First we get the task to write a function that doubles all elements of a `[Integer]` list.
We want to reuse the `double` function we have already defined above.
With all that we have learnt so far writing a function `doubleAll` isn't that hard:
```haskell
-- compute the double value for all list elements
doubleAll :: [Integer] -> [Integer]
doubleAll [] = []
doubleAll (n:rest) = double n : doubleAll rest
```
Next we are asked to implement a similar function `squareAll` that will use `square` to compute the square of all elements in a list.
The naive way would be to implement it in the *WET* (We Enjoy Typing) approach:
```haskell
-- compute squares for all list elements
squareAll :: [Integer] -> [Integer]
squareAll [] = []
squareAll (n:rest) = square n : squareAll rest
```
Of course this is very ugly:
both function use the same pattern matching and apply the same recursive iteration strategy.
They only differ in the function applied to each element.
As role model developers we don't want to repeat ourselves. We are thus looking for something that
captures the essence of mapping a given function over a list of elements:
```haskell
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
```
This function abstracts away the implementation details of iterating over a list and allows to provide a user defined
mapping function as well.
Now we can use `map` to simply *declare our intention* (the 'what') and don't have to detail the 'how':
```haskell
doubleAll' :: [Integer] -> [Integer]
doubleAll' = map double
squareAll' :: [Integer] -> [Integer]
squareAll' = map square
```
### folding and reducing
Now let's have a look at some related problem.
Our first task is to add up all elements of a `[Integer]` list.
First the naive approach which uses the already familiar mix of pattern matching plus recursion:
```haskell
sumUp :: [Integer] -> Integer
sumUp [] = 0
sumUp (n:rest) = n + sumUp rest
```
By looking at the code for a function that computes the product of all elements of a `[Integer]` list we can again see that
we are repeating ourselves:
```haskell
prod :: [Integer] -> Integer
prod [] = 1
prod (n:rest) = n * prod rest
```
So what is the essence of both algorithms?
At the core of both algorithms we have a recursive function which
- takes a binary operator (`(+)`or `(*)` in our case),
- an initial value that is used as a starting point for the accumulation
(typically the identity element (or neutral element) of the binary operator),
- the list of elements that should be reduced to a single return value
- performs the accumulation by recursively applying the binary operator to all elements of the list until the `[]` is reached,
where the neutral element is returned.
This essence is contained in the higher order function `foldr` which again is part of the Haskell standard library:
```haskell
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f acc [] = acc
foldr f acc (x:xs) = f x (foldr f acc xs)
```
Now we can use `foldr` to simply *declare our intention* (the 'what') and don't have to detail the 'how':
```haskell
sumUp' :: [Integer] -> Integer
sumUp' = foldr (+) 0
prod' :: [Integer] -> Integer
prod' = foldr (*) 1
```
---

View File

@ -1,8 +1,8 @@
module Lists where
import Prelude hiding (map, foldr)
import Prelude hiding (map, foldr, length, filter)
import qualified Prelude as P (foldr)
import Functions (square)
import Functions (square, double)
-- a list of numbers to play around with
someNumbers :: [Integer]
@ -15,28 +15,33 @@ upToHundred = [1..100]
oddsUpToHundred :: [Integer]
oddsUpToHundred = [1,3..100]
length
length :: [a] -> Integer
length [] = 0
length (x:xs) = 1 + length xs
-- | Extract the first element of a list, which must be non-empty.
head :: [a] -> a
head (x:_) = x
head [] = error "head: empty list"
filter :: (a -> Bool) -> [a] -> [a]
filter _pred [] = []
filter pred (x:xs)
| pred x = x : filter pred xs
| otherwise = filter pred xs
-- filtering lists
-- | Extract the elements after the head of a list, which must be non-empty.
tail :: [a] -> [a]
tail (_:xs) = xs
tail [] = error "tail: empty list"
someEvenNumbers :: [Integer]
someEvenNumbers = filter even someNumbers
someOddNumbers :: [Integer]
someOddNumbers = filter (\n -> n `rem` 2 /= 0) someNumbers
-- compute squares for all list elements
squareAll :: [Integer] -> [Integer]
squareAll [] = []
squareAll (n:rest) = square n : squareAll rest
-- compute triples for all list elements
tripleAll :: [Integer] -> [Integer]
tripleAll [] = []
tripleAll (n:rest) = (\i -> i*i*i) n : tripleAll rest
-- compute the double value for all list elements
doubleAll :: [Integer] -> [Integer]
doubleAll [] = []
doubleAll (n:rest) = double n : doubleAll rest
-- We don't want to repeat ourselves so we want something that captures the essence of mapping a function over a list:
map :: (a -> b) -> [a] -> [b]
@ -84,12 +89,6 @@ prod' = foldr (*) 1
foldMap :: (Monoid m) => (a -> m) -> [a] -> m
foldMap f = foldr (mappend . f) mempty
-- filtering lists
-- filter :: (a -> Bool) -> [a] -> [a]
someEvenNumbers :: [Integer]
someEvenNumbers = filter even someNumbers
someOddNumbers :: [Integer]
someOddNumbers = filter (\n -> n `rem` 2 /= 0) someNumbers