denisshevchenko/ohaskell.guide

View on GitHub
chapters/08-choose-n-patterns.md

Summary

Maintainability
Test Coverage
# Выбор и образцы

Эта глава откроет нам другие способы выбора, а также познакомит нас с образцами. Уверяю, вы влюбитесь в них!

## Не только из двух

Часто мы хотим выбирать не только из двух возможных вариантов. Вот как это можно сделать:

```haskell
analyzeGold :: Int -> String
analyzeGold standard =
  if standard == 999
    then "Wow! 999 standard!"
    else if standard == 750
           then "Great! 750 standard."
           else if standard == 585
                  then "Not bad! 585 standard."
                  else "I don't know such a standard..."

main :: IO ()
main = putStrLn (analyzeGold 999)
```

Уверен, вы уже стираете плевок с экрана. Вложенная `if-then-else` конструкция не может понравиться никому, ведь она крайне неудобна в обращении. А уж если бы анализируемых проб золота было штук пять или семь, эта лестница стала бы поистине ужасной. К счастью, в Haskell можно написать по-другому:

```haskell
analyzeGold :: Int -> String
analyzeGold standard =
  if | standard == 999 -> "Wow! 999 standard!"
     | standard == 750 -> "Great! 750 standard."
     | standard == 585 -> "Not bad! 585 standard."
     | otherwise -> "I don't know such a standard..."
```

Не правда ли, так красивее? Это — множественный `if`. Работает он по схеме:

```haskell
if | COND1 -> EXPR1
   | COND2 -> EXPR2
   | ...
   | CONDn -> EXPRn
   | otherwise -> COMMON_EXPR
```

где `COND1..n` — выражения, дающие ложь или истину, а `EXPR1..n` — соответствующие им результирующие выражения. Особая функция `otherwise` соответствует общему случаю, когда ни одно из логических выражений не дало `True`, и в этой ситуации результатом условной конструкции послужит выражение `COMMON_EXPR`.

Не пренебрегайте `otherwise`! Если вы его не укажете и при этом примените функцию `analyzeGold` к значению, отличному от проверяемых:

```haskell
analyzeGold :: Int -> String
analyzeGold standard =
  if | standard == 999 -> "Wow! 999 standard!"
     | standard == 750 -> "Great! 750 standard."
     | standard == 585 -> "Not bad! 585 standard."

main :: IO ()
main = putStrLn (analyzeGold 583)  -- Ой...
```

компиляция завершится успешно, однако в момент запуска программы вас ожидает неприятный сюрприз в виде ошибки:

```bash
Non-exhaustive guards in multi-way if
```

Проверка получилась неполной, вот и получите ошибку.

Кстати, видите слово `guards` в сообщении об ошибке? Вертикальные черты перед логическими выражениями — это и есть охранники (англ. guard), неусыпно охраняющие наши условия. Потешное название выбрали. Чтобы читать их было легче, воспринимайте их как аналог слова «ИЛИ».

А сейчас стоп. Вы ведь попробовали скомпилировать этот код, не так ли? А почему вы не ругаетесь? Ведь такой код не скомпилируется, так как не хватает одной маленькой, но важной детали. Вот как должен выглядеть модуль `Main`:

```haskell
{-# LANGUAGE MultiWayIf #-}  -- Что это??

module Main where

analyzeGold :: Int -> String
analyzeGold standard =
  if | standard == 999 -> "Wow! 999 standard!"
     | standard == 750 -> "Great! 750 standard."
     | standard == 585 -> "Not bad! 585 standard."
     | otherwise -> "I don't know such a standard..."

main :: IO ()
main = putStrLn (analyzeGold 999)
```

Вот теперь всё в порядке. Но что это за странный комментарий в первой строке модуля? Вроде бы оформлен как многострочный комментарий, но выглядит необычно. Перед нами — указание расширения языка Haskell.

Стандарт [Haskell 2010](https://www.haskell.org/onlinereport/haskell2010/) — это официальный стержень языка. Однако компилятор GHC, давно уж ставший компилятором по умолчанию при разработке на Haskell, обладает рядом особых возможностей. По умолчанию многие из этих возможностей выключены, а прагма `LANGUAGE` как раз для того и предназначена, чтобы их включать/активизировать. В данном случае мы включили расширение `MultiWayIf`. Именно это расширение позволяет нам использовать множественный `if`. Такого рода расширений существует очень много, и мы будем часто их использовать.

Помните: расширение, включённое с помощью прагмы `LANGUAGE`, действует лишь в рамках текущего модуля. И если я прописал его только в модуле `app/Main.hs`, то на модуль `src/Lib.hs` механизм `MultiWayIf` не распространяется.

## Без Если

Множественный `if` весьма удобен, но есть способ более красивый. Взгляните:

```haskell
analyzeGold :: Int -> String
analyzeGold standard
  | standard == 999 = "Wow! 999 standard!"
  | standard == 750 = "Great! 750 standard."
  | standard == 585 = "Not bad! 585 standard."
  | otherwise = "I don't know such a standard..."
```

Ключевое слово `if` исчезло. Схема здесь такая:

```haskell
function arg  -- Нет знака равенства?
  | COND1 = EXPR1
  | COND2 = EXPR2
  | ...
  | CONDn = EXPRn
  | otherwise = COMMON_EXPR
```

Устройство почти такое же, но, помимо исчезновения ключевого слова `if`, мы теперь используем знаки равенства вместо стрелок. Именно поэтому исчез знакомый нам знак равенства после имени аргумента `arg`. В действительности он, конечно, никуда не исчез, он лишь перешёл в выражения. А чтобы это легче прочесть, напишем выражения в строчку:

```haskell
function arg  |    COND1     =      EXPR1     |    ...

эта           или            равна
функция
                                    этому
                                    выражению

                   в случае
                   истинности
                   этого
                   выражения
                                              или  и т.д.
```

То есть перед нами уже не одно определение функции, а цепочка определений, потому нам и не нужно ключевое слово `if`. Но и эту цепочку определений можно упростить.

## Сравнение с образцом

Убрав слово `if`, мы и с нашими виртуальными «ИЛИ» можем расстаться. В этом случае останется лишь это:

```haskell
analyzeGold :: Int -> String  -- Одно объявление.
-- И множество определений...
analyzeGold 999 = "Wow! 999 standard!"
analyzeGold 750 = "Great! 750 standard."
analyzeGold 585 = "Not bad! 585 standard."
analyzeGold _   = "I don't know such a standard..."
```

Мы просто перечислили определения функции `analyzeGold` одно за другим. На первый взгляд, возможность множества определений одной и той же функции удивляет, но если вспомнить, что применение функции суть выражение, тогда ничего удивительного. Вот как это читается:

```haskell
      analyzeGold  999          =      "Wow! 999 standard!"

если  эта функция  применяется  тогда  этому выражению
                   к этому      она
                   аргументу    равна

      analyzeGold  750          =      "Great! 750 standard."

если  эта функция  применяется  тогда  другому выражению
                   к другому    она
                   аргументу    равна
...
       analyzeGold _ =      "I don't know such a standard..."

иначе  эта функция   равна  общему выражению
```

Когда функция `analyzeGold` применяется к конкретному аргументу, этот аргумент последовательно сравнивается с образцом (англ. pattern matching). Образца здесь три: `999`, `750` и `585`. И если раньше мы сравнивали аргумент с этими числовыми значениями явно, посредством функции `==`, теперь это происходит скрыто. Идея сравнения с образцом очень проста: что-то (в данном случае реальный аргумент) сопоставляется с образцом (или образцами) на предмет «подходит/не подходит». Если подходит — то есть сравнение с образцом даёт результат `True` — готово, используем соответствующее выражение. Если же не подходит — переходим к следующему образцу.

Сравнение с образцом, называемое ещё «сопоставлением с образцом» используется в Haskell чрезвычайно широко. В русскоязычной литературе перевод словосочетания «pattern matching» не особо закрепился, вместо этого так и говорят «паттерн матчинг». Я поступлю так же.

Но что это за символ подчёркивания такой, в последнем варианте определения? Вот этот:

```haskell
analyzeGold _ = "I don't know such a standard..."
            ^
```

С формальной точки зрения, это — универсальный образец, сравнение с которым всегда истинно (ещё говорят, что с ним матчится (англ. match) всё что угодно). А с неформальной — это символ, который можно прочесть как «мне всё равно». Мы как бы говорим: «В данном случае нас не интересует конкретное содержимое аргумента, нам всё равно, мы просто возвращаем строку `I don't know such a standard...`».

Важно отметить, что сравнение аргумента с образцами происходит последовательно, сверху вниз. Поэтому если мы напишем так:

```haskell
analyzeGold :: Int -> String
analyzeGold _   = "I don't know such a standard..."
analyzeGold 999 = "Wow! 999 standard!"
analyzeGold 750 = "Great! 750 standard."
analyzeGold 585 = "Not bad! 585 standard."
```

наша функция будет всегда возвращать первое выражение, строку `I don't know such a standard...`, и это вполне ожидаемо: первая же проверка гарантированно даст нам `True`, ведь с образцом `_` совпадает всё что угодно. Таким образом, общий образец следует располагать в самом конце, чтобы мы попали на него лишь после того, как не сработали все остальные образцы.

## case

Существует ещё один вид паттерн матчинга, с помощью конструкции `case-of`:

```haskell
analyzeGold standard =
  case standard of
    999 -> "Wow! 999 standard!"
    750 -> "Great! 750 standard."
    585 -> "Not bad! 585 standard."
    _   -> "I don't know such a standard..."
```

Запомните конструкцию `case-of`, мы встретимся с нею не раз. Работает она по модели:

```haskell
case EXPRESSION of
  PATTERN1 -> EXPR1
  PATTERN2 -> EXPR2
  ...
  PATTERNn -> EXPRn
  _        -> COMMON_EXPR
```

где `EXPRESSION` — анализируемое выражение, последовательно сравниваемое с образцами `PATTERN1..n`. Если ни одно не сработало — как обычно, упираемся в универсальный образец `_` и выдаём `COMMON_EXPR`.

В последующих главах мы встретимся и с другими видами паттерн матчинга, ведь он используется не только для выбора.