Skip to content

Commit

Permalink
rework section about asking questions about values
Browse files Browse the repository at this point in the history
The exercises for this previously involved actually not asking questions
about values, so I've instead deemphasized the code reuse part of that.

The explanations were also rewritten to not involve any kind of custom
data definition, as this kind of thing will be explored in the next
chapter instead.
  • Loading branch information
GoNZooo committed Aug 14, 2021
1 parent c5c31af commit 462af9d
Showing 1 changed file with 24 additions and 108 deletions.
132 changes: 24 additions & 108 deletions basics/01-values-and-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Guards](#guards)
- [Multi-way `if`](#multi-way-if)
- [`case` expressions](#case-expressions)
- [Top-level pattern matching](#top-level-pattern-matching)
- [Exercises (Asking questions about values)](#exercises-asking-questions-about-values)
- [Exercise notes (Asking questions about values)](#exercise-notes-asking-questions-about-values)
- [Interlude: Function application with `$`](#interlude-function-application-with-)
Expand Down Expand Up @@ -349,122 +350,39 @@ What we have above is a generalized `if` that supports many branches with arbitr
called a "multi-way `if`" and is enabled by the `MultiWayIf` extension, which we've enabled by
default in our templates.

While we need to be aware that this does require an extension, it's still useful to learn and use
this as a standard part of the language. Why? Because it's very flexible and solves the issue of
having to introduce another, more roundabout way of being able to use guards:

```haskell
import Prelude

-- | Limits a given integer to be within the range @lowerBound <= value <= upperBound@.
clamp :: Int -> Int -> Int -> Int
clamp lowerBound upperBound value =
case () of
() | value < lowerBound -> lowerBound
() | value > upperBound -> upperBound
() | otherwise -> value
```

Because `case` expressions allow us to use guards we can introduce this `case` expression context
where we don't actually pattern match on anything but still use the guards. This is obviously not
ideal and when we see this it's much better to just use multi-way `if`.

`case` expressions and their cousin, top-level pattern matching, are ideal for when we want to match
on the structure of a given value, either with literal values exactly or want to pull apart some
structure via their constructors and field names:

```haskell
import Prelude

data DivisionResult
= DivideSuccess Float
| DivisionByZero
deriving (Show)

safeDivide :: Int -> Int -> DivisionResult
safeDivide _x 0 = DivisionByZero
safeDivide x divisor =
let xAsFloat = fromIntegral x
divisorAsFloat = fromIntegral divisor
in DivideSuccess (xAsFloat / divisorAsFloat)
```

We will go deeper into how to use `data` in the [next chapter](./02-composite-datatypes.md) but for
now all we need to know is that `safeDivide` can either return a `DivisionByZero` result or a
`DivideSuccess` result that also carries a float with it.

If we execute this function we can see the following:

```haskell
Q> safeDivide 5 0
DivisionByZero
Q> safeDivide 5 2
DivideSuccess 2.5
```

### `case` expressions

We can use `case` to immediately ask questions about this structure:
`case` expressions allow us to ask questions about the structure of values:

```haskell
import Prelude

usingSafeDivide :: Int -> Int -> String
usingSafeDivide x divisor =
-- Note how we use `case` here to deconstruct the result
case safeDivide x divisor of
DivideSuccess result ->
-- `show` takes a "showable" value and turns it into a `String`
-- `<>` here is a way to concatenate two strings together
"Your result was: " <> show result
DivisionByZero ->
"You tried to divide by zero"

data DivisionResult
= DivideSuccess Float
| DivisionByZero
deriving (Show)

-- Note that we are using something called "top-level pattern matching" here: We have two clauses
-- for our `safeDivide` function; one if the divisor is the number `0` where we will always return
-- `DivisionByZero` and one case for all other divisors where we do the actual calculation.
safeDivide :: Int -> Int -> DivisionResult
safeDivide _x 0 = DivisionByZero
safeDivide x divisor =
let xAsFloat = fromIntegral x
divisorAsFloat = fromIntegral divisor
in DivideSuccess (xAsFloat / divisorAsFloat)
addFileExtension :: String -> String
addFileExtension filename = case filename of
"README" -> "README.md"
"LICENSE" -> "LICENSE.txt"
other -> other
```

And when we run our `usingSafeDivide` function:
In the above example we can see that we are looking at the literal value of the filename that is
passed to the function and if it matches the two literal values that are stated we execute a
specific expression for each case. Like guards, these are tried in order, so being as specific as
you can and keeping the more general cases last is recommended.

```haskell
Q> usingSafeDivide 5 0
"You tried to divide by zero"
Q> usingSafeDivide 5 2
"Your result was: 2.5"
```
### Top-level pattern matching

You may have noticed that I have referred to `case` as an expression; this is not a mistake. If we
change the above code example to use the following we can see that indeed, `case` is an expression
like everything else:
There is a more direct way to write this particular case expression, using something known as
"top-level pattern matching":

```haskell
putStrLn $ case safeDivide x divisor of
DivideSuccess result ->
"Your result was: " <> show result
DivisionByZero ->
"You tried to divide by zero"
addFileExtension' :: String -> String
addFileExtension' "README" = "README.md"
addFileExtension' "LICENSE" = "LICENSE.txt"
addFileExtension' filename = filename
```

Here we are saying that `putStrLn` will take whatever our `case` expression returns, meaning it in
this case always will have to return a `String`. What this means is that `case` expressions have to
return values of the same type in all branches and there is no "empty case" where we return `void`
or the like.

As we saw in the previous example we can execute actions in our case branches. That example, where
we printed a string in each branch of the `case`, worked because we were constructing an
action of type `IO ()` in each branch when we executed `putStrLn ...`.
Top-level pattern matching means that we can pattern match just like we would in a case expression
but do it with our function argument bindings. In this example we are saying, much like in the
previous example, that if the filename is README or LICENSE we want to execute specific logic for
those, but we also have a general case the comes last so that every input value is handled.

### Exercises (Asking questions about values)

Expand All @@ -475,15 +393,13 @@ action of type `IO ()` in each branch when we executed `putStrLn ...`.
with function guards as well as `if`.

3. Define a function that subtracts an integer from another, but if the result is less than zero,
instead return `0`. Use the function you defined in exercise 1.
instead return `0`.

4. Define a function that takes an `Int` and if it's smaller than zero returns `0`, if it's bigger
than 255 returns `255`. Use the functions you defined in exercises 1 and 2[0].
than 255 returns `255`.

#### Exercise notes (Asking questions about values)

0. You don't need `if` for this one.

## Interlude: Function application with `$`

Sometimes you'll want to apply a function and you'll need to parenthesize the expression in order
Expand Down

0 comments on commit 462af9d

Please sign in to comment.