denisshevchenko/ohaskell.guide

View on GitHub
chapters/12-tuple.md

Summary

Maintainability
Test Coverage
# Кортеж

В этой главе мы познакомимся с кортежем и ещё ближе подружимся с паттерн матчингом.

Кортеж (англ. tuple) — ещё одна стандартная структура данных, но, в отличие от списка, она может содержать данные как одного типа, так и разных.

Структуры, способные содержать данные разных типов, называют гетерогенными (в переводе с греческого: «разного рода»).

Вот как выглядит кортеж:

```haskell
("Haskell", 2010)
```

Круглые скобки и значения, разделённые запятыми. Этот кортеж содержит значение типа `String` и ещё одно, типа `Int`. Вот ещё пример:

```haskell
("Haskell", "2010", "Standard")
```

То есть ничто не мешает нам хранить в кортеже данные одного типа.

## Тип кортежа

Тип списка строк, как вы помните, `[String]`. И не важно, сколько строк мы запихнули в список, одну или миллион — его тип останется неизменным. С кортежем же дело обстоит абсолютно иначе.

Тип кортежа зависит от количества его элементов. Вот тип кортежа, содержащего две строки:

```haskell
(String, String)
```

Вот ещё пример:

```haskell
(Double, Double, Int)
```

И ещё:

```haskell
(Bool, Double, Int, String)
```

Тип кортежа явно отражает его содержимое. Поэтому если функция применяется к кортежу из двух строк, применить её к кортежу из трёх никак не получится, ведь типы этих кортежей различаются:

```haskell
-- Разные типы
(String, String)
(String, String, String)
```

## Действия над кортежами

Со списками можно делать много всего, а вот с кортежами — не очень. Самые частые действия — собственно формирование кортежа и извлечение хранящихся в нём данных. Например:

```haskell
makeAlias :: String -> String -> (String, String)
makeAlias host alias = (host, alias)
```

Пожалуй, ничего проще придумать нельзя: на входе два аргумента, на выходе — двухэлементный кортеж с этими аргументами. Двухэлементный кортеж называют ещё парой (англ. pair). И хотя кортеж может содержать сколько угодно элементов, на практике именно пары встречаются чаще всего.

Обратите внимание, насколько легко создаётся кортеж. Причина тому — уже знакомый нам паттерн матчинг:

```haskell
makeAlias host alias = (host, alias)

          ____          ____

               =====          =====
```

Мы просто указываем соответствие между левой и правой сторонами определения: «Пусть первый элемент пары будет равен аргументу `host`, а второй — аргументу `alias`». Ничего удобнее и проще и придумать нельзя. И если бы мы хотели получить кортеж из трёх элементов, это выглядело бы так:

```haskell
makeAlias :: String -> String -> (String, String, String)
makeAlias host alias = (host, "https://" ++ host, alias)

          ____          ____                ____

               =====                              =====
```

Оператор `++` — это оператор конкатенации, склеивающий две строки в одну. Строго говоря, он склеивает два списка, но мы-то с вами уже знаем, что `String` есть ни что иное, как `[Char]`. Таким образом, `"https://"` ++ `"www.google.com"` даёт нам `"https://www.google.com"`.

Извлечение элементов из кортежа также производится через паттерн матчинг:

```haskell
main :: IO ()
main =
  let (host, alias) = makeAlias "173.194.71.106"
                                "www.google.com"
  in print (host ++ ", " ++ alias)
```

Функция `makeAlias` даёт нам пару из хоста и имени. Но что это за странная запись возле уже знакомого нам слова `let`? Это промежуточное выражение, но выражение хитрое, образованное через паттерн матчинг. Чтобы было понятнее, сначала перепишем функцию без него:

```haskell
main :: IO ()
main =
  let pair  = makeAlias "173.194.71.106"
                        "www.google.com"
      host  = fst pair  -- Берём первое...
      alias = snd pair  -- Берём второе...
  in print (host ++ ", " ++ alias)
```

При запуске этой программы получим:

```bash
"173.194.71.106, www.google.com"
```

Стандартные функции `fst` и `snd` возвращают первый и второй элемент кортежа соответственно. Выражение `pair` соответствует паре, выражение `host` — значению хоста, а `alias` — значению имени. Но не кажется ли вам такой способ избыточным? Мы в Haskell любим изящные решения, поэтому предпочитаем паттерн матчинг. Вот как получается вышеприведённый способ:

```haskell
let (host, alias) = makeAlias "173.194.71.106" "www.google.com"

let (host, alias) = ("173.194.71.106", "www.google.com")

                     данное значение
     это
     хост
                                       а вот это значение
           это
           имя
```

Вот такая простая магия. Функция `makeAlias` даёт нам пару, и мы достоверно знаем это! А если знаем, нам не нужно вводить промежуточные выражения вроде `pair`. Мы сразу говорим:

```haskell
let (host, alias) = makeAlias "173.194.71.106" "www.google.com"

                    мы точно знаем, что выражение,
                    вычисленное этой функцией
     это вот
     такая пара
```

Это «зеркальная» модель: через паттерн матчинг формируем:

```haskell
-- Формируем правую сторону
-- на основе левой...
host alias = (host, alias)

>>>>          >>>>

     >>>>>          >>>>>
```

и через него же извлекаем:

```haskell
-- Формируем левую сторону
-- на основе правой...
(host, alias) = ("173.194.71.106", "www.google.com")

 <<<<            <<<<<<<<<<<<<<<<

       <<<<<                       <<<<<<<<<<<<<<<<
```

Вот ещё один пример работы с кортежем через паттерн матчинг:

```haskell
chessMove :: String
          -> (String, String)
          -> (String, (String, String))
chessMove color (from, to) = (color, (from, to))

main :: IO ()
main = print (color ++ ": " ++ from ++ "-" ++ to)
  where
    (color, (from, to)) = chessMove "white" ("e2", "e4")
```

И на выходе получаем:

```bash
"white: e2-e4"
```

Обратите внимание, объявление функции отформатировано чуток иначе: типы выстроены друг под другом через выравнивание стрелок под двоеточием. Вы часто встретите такой стиль в Haskell-проектах.

Функция `chessMove` даёт нам кортеж с кортежем, а раз мы точно знаем вид этого кортежа, сразу указываем `where`-выражение в виде образца:

```haskell
(color, (from, to)) = chessMove "white" ("e2", "e4")

 _____                          _______

         ====                            ====

               ..                              ....
```

## Не всё

Мы можем вытаскивать по образцу лишь часть нужной нам информации. Помните универсальный образец `_`? Взгляните:

```haskell
-- Поясняющие псевдонимы
type UUID     = String
type FullName = String
type Email    = String
type Age      = Int
type Patient = (UUID, FullName, Email, Age)

patientEmail :: Patient -> Email
patientEmail (_, _, email, _) = email

main :: IO ()
main =
  putStrLn (patientEmail ( "63ab89d"
                         , "John Smith"
                         , "johnsm@gmail.com"
                         , 59
                         ))
```

Функция `patientEmail` даёт нам почту пациента. Тип `Patient` &mdash; это псевдоним для кортежа из четырёх элементов: уникальный идентификатор, полное имя, адрес почты и возраст. Дополнительные псевдонимы делают наш код яснее: одно дело видеть безликую `String` и совсем другое &mdash; `Email`.

Рассмотрим внутренность функции `patientEmail`:

```haskell
patientEmail (_, _, email, _) = email
```

Функция говорит нам: &laquo;Да, я знаю, что мой аргумент &mdash; это четырёхэлементный кортеж, но меня в нём интересует исключительно третий по счёту элемент, соответствующий адресу почты, его я и верну&raquo;. Универсальный образец `_` делает наш код лаконичнее и понятнее, ведь он помогает нам игнорировать то, что нам неинтересно. Строго говоря, мы не обязаны использовать `_`, но с ним будет лучше.

## А если ошиблись?

При использовании паттерн матчинга в отношении пары следует быть внимательным. Представим себе, что вышеупомянутый тип `Patient` был расширен:

```haskell
type UUID      = String
type FullName  = String
type Email     = String
type Age       = Int
type DiseaseId = Int  -- Новый элемент.
type Patient = ( UUID
               , FullName
               , Email
               , Age
               , DiseaseId
               )
```

Был добавлен идентификатор заболевания. И всё бы хорошо, но внести изменения в функцию `patientEmail` мы забыли:

```haskell
patientEmail :: Patient -> Email
patientEmail (_, _, email, _) = email

              ^  ^  ^      ^  -- А пятый где?
```

К счастью, в этом случае компилятор строго обратит наше внимание на ошибку:

```bash
Couldn't match type ‘(t0, t1, String, t2)’
               with ‘(UUID, FullName, Email, Age, DiseaseId)’
Expected type: Patient
  Actual type: (t0, t1, String, t2)
In the pattern: (_, _, email, _)
```

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

На этом наше знакомство с кортежем считаю завершённым, в последующих главах мы будем использовать их периодически.

## Для любопытных

Для работы с элементами многоэлементных кортежей можно использовать готовые библиотеки, во избежании длинных паттерн матчинговых цепочек. Например, пакет [tuple](http://hackage.haskell.org/package/tuple):

```haskell
Data.Tuple.Select

main :: IO ()
main = print (sel4 (123, 7, "hydra", "DC:4", 44, "12.04"))
```

Функция `sel4` из модуля `Data.Tuple.Select` извлекает четвёртый по счёту элемент кортежа, в данном случае строку `"DC:4"`. Там есть функции вплоть до `sel32`, авторы вполне разумно сочли, что никто, находясь в здравом уме и твёрдой памяти, не станет оперировать кортежами, состоящими из более чем 32 элементов.

Кроме того, мы и обновлять элементы кортежа можем:

```haskell
import Data.Tuple.Update

main :: IO ()
main = print (upd2 2 ("si", 45))
```

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

```bash
("si",2)
```

Второй элемент кортежа изменился с `45` на `2`.