denisshevchenko/ohaskell.guide

View on GitHub
chapters/20-adt.md

Summary

Maintainability
Test Coverage
# АТД

АТД, или Алгебраические Типы Данных (англ. ADT, Algebraic Data Type), занимают почётное место в мире типов Haskell. Абсолютно подавляющее большинство ваших собственных типов будут алгебраическими, и то же можно сказать о типах из множества Haskell-пакетов. Алгебраическим типом данных называют такой тип, который составлен из других типов. Мы берём простые типы и строим из них, как из кирпичей, типы сложные, а из них — ещё более сложные. Это даёт нам невероятный простор для творчества.

Оставим сетевые протоколы и дни недели, рассмотрим такой пример:

```haskell
data IPAddress = IPAddress String
```

Тип `IPAddress` использует один-единственный конструктор значения, но кое-что изменилось. Во-первых, имена типа и конструктора совпадают. Это вполне легально, вы встретите такое не раз. Во-вторых, конструктор уже не нульарный, а унарный (англ. unary), потому что теперь он связан с одним значением типа `String`. И вот как создаются значения типа `IPAddress`:

```haskell
  let ip = IPAddress "127.0.0.1"
```

Значение `ip` типа `IPAddress` образовано конструктором и конкретным значением некоего типа:

```haskell
  let ip = IPAddress       "127.0.0.1"

           конструктор     значение
           значения        типа
           типа IPAddress  String

           └ значение типа IPAddress ┘
```

Значение внутри нашего типа называют ещё полем (англ. field):

```haskell
data IPAddress = IPAddress    String

     тип         конструктор  поле
```

Расширим тип `IPAddress`, сделав его более современным:

```haskell
data IPAddress = IPv4 String | IPv6 String
```

Теперь у нас два конструктора, соответствующих разным IP-версиям. Это позволит нам создавать значение типа `IPAddress` так:

```haskell
  let ip = IPv4 "127.0.0.1"
```

или так:

```haskell
  let ip = IPv6 "2001:0db8:0000:0042:0000:8a2e:0370:7334"
```

Сделаем тип ещё более удобным. Так, при работе с IP-адресом нам часто требуется `localhost`. И чтобы явно не писать `"127.0.0.1"` и `"0:0:0:0:0:0:0:1"`, введём ещё два конструктора:

```haskell
data IPAddress = IPv4 String
               | IPv4Localhost
               | IPv6 String
               | IPv6Localhost
```

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

```haskell
    let ip = IPv4Localhost
```

## Извлекаем значение

Допустим, мы создали значение `google`:

```haskell
  let google = IPv4 "173.194.122.194"
```

Как же нам потом извлечь конкретное строковое значение из `google`? С помощью нашего старого друга, паттерн матчинга:

```haskell
checkIP :: IPAddress -> String
checkIP (IPv4 address) = "IP is '" ++ address ++ "'."

main :: IO ()
main = putStrLn . checkIP $ IPv4 "173.194.122.194"
```

Результат:

```bash
IP is '173.194.122.194'.
```

Взглянем на определение:

```haskell
checkIP (IPv4 address) = "IP is '" ++ address ++ "'."
```

Здесь мы говорим: «Мы знаем, что значение типа `IPAddress` сформировано с конструктором и строкой». Однако внимательный компилятор сделает нам замечание:

```bash
Pattern match(es) are non-exhaustive
In an equation for ‘checkIP’:
    Patterns not matched:
        IPv4Localhost
        IPv6 _
        IPv6Localhost
```

В самом деле, откуда мы знаем, что значение, к которому применили функцию `checkIP`, было сформировано именно с помощью конструктора `IPv4`? У нас же есть ещё три конструктора, и нам следует проверить их все:

```haskell
checkIP :: IPAddress -> String
checkIP (IPv4 address) = "IPv4 is '" ++ address ++ "'."
checkIP IPv4Localhost  = "IPv4, localhost."
checkIP (IPv6 address) = "IPv6 is '" ++ address ++ "'."
checkIP IPv6Localhost  = "IPv6, localhost."
```

С каким конструктором совпало — с таким и было создано значение. Можно, конечно, и так проверить:

```haskell
checkIP :: IPAddress -> String
checkIP addr = case addr of
    IPv4 address  -> "IPv4 is '" ++ address ++ "'."
    IPv4Localhost -> "IPv4, localhost."
    IPv6 address  -> "IPv6 is '" ++ address ++ "'."
    IPv6Localhost -> "IPv6, localhost."
```

## Строим

Определим тип для сетевой точки:

```haskell
data EndPoint = EndPoint String Int
```

Конструктор `EndPoint` — бинарный, ведь здесь уже два значения. Создаём обычным образом:

```haskell
  let googlePoint = EndPoint "173.194.122.194" 80
```

Конкретные значения извлекаем опять-таки через паттерн матчинг:

```haskell
main :: IO ()
main = putStrLn $ "The host is: " ++ host
  where
    EndPoint host _ = EndPoint "173.194.122.194" 80

    └── образец ──┘   └──────── значение ─────────┘
```

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

И всё бы хорошо, но тип `EndPoint` мне не очень нравится. Есть в нём что-то некрасивое. Первым полем выступает строка, содержащая IP-адрес, но зачем нам строка? У нас же есть прекрасный тип `IPAddress`, он куда лучше безликой строки. Это общее правило для Haskell-разработчика: чем больше информации несёт в себе тип, тем он лучше. Давайте заменим определение:

```haskell
data EndPoint = EndPoint IPAddress Int
```

Тип стал понятнее, и вот как мы теперь будем создавать значения:

```haskell
  let google = EndPoint (IPv4 "173.194.122.194") 80
```

Красиво. Извлекать конкретные значения будем так:

```haskell
main :: IO ()
main = putStrLn $ "The host is: " ++ ip
  where
    EndPoint (IPv4 ip) _ = EndPoint (IPv4 "173.194.122.194") 80
              ____                   ____

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

Здесь мы опять-таки игнорируем порт, но значение IP-адреса извлекаем уже на основе образца с конструктором `IPv4`.

Это простой пример того, как из простых типов строятся более сложные. Но сложный тип вовсе не означает сложную работу с ним, паттерн матчинг элегантен как всегда. А вскоре мы узнаем о другом способе работы с полями типов, без паттерн матчинга.

Любопытно, что конструкторы типов тоже можно компоновать, взгляните:

```haskell
main :: IO ()
main = putStrLn $ "The host is: " ++ ip
  where
    EndPoint (IPv4 ip) _ = (EndPoint . IPv4 $ "173.194.122.194") 80
```

Это похоже на маленькое волшебство, но конструкторы типов можно компоновать знакомым нам оператором композиции функций:

```haskell
(EndPoint . IPv4 $ "173.194.122.194") 80

            │       значение типа      │
            └──────── IPAddress ───────┘
```

Вам это ничего не напоминает? Это же в точности так, как мы работали с функциями! Из этого мы делаем вывод: конструктор значения можно рассматривать как особую функцию. В самом деле:

```haskell
EndPoint   (IPv4 "173.194.122.194")  80

"функция"  │        первый        │  второй
           └────── аргумент ──────┘  аргумент
```

Мы как бы применяем конструктор к конкретным значениям как к аргументам, в результате чего получаем значение нашего типа. А раз так, мы можем компоновать конструкторы так же, как и обычные функции, лишь бы их типы были комбинируемыми. В данном случае всё в порядке: тип значения, возвращаемого конструктором `IPv4`, совпадает с типом первого аргумента конструктора `EndPoint`.

Вот мы и познакомились с настоящими типами. Пришло время узнать о более удобной работе с полями типов.