Главная / Курсы / О Haskell по-человечески / Алгебраические типы данных
# Глава 20. Алгебраические типы данных АТД, или Алгебраические Типы Данных (англ. ADT, Algebraic Data Type), занимают почётное место в мире типов Haskell. Подавляющее большинство ваших собственных типов будут алгебраическими, и то же можно сказать о типах из множества 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 {.example_for_playground .example_for_playground_001} 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` сформировано с конструктором и строкой». Однако внимательный компилятор сделает нам замечание: ``` 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 {.example_for_playground .example_for_playground_002} main :: IO () main = putStrLn $ "The host is: " ++ ip where EndPoint (IPv4 ip) _ = EndPoint (IPv4 "173.194.122.194") 80 ``` Здесь мы опять-таки игнорируем порт, но значение IP-адреса извлекаем уже на основе образца с конструктором `IPv4`. Это пример того, как из простых типов строятся более сложные. Но сложный тип вовсе не означает сложную работу с ним, паттерн матчинг элегантен как всегда. А вскоре мы узнаем о другом способе работы с полями типов, без паттерн матчинга. Любопытно, что конструкторы типов тоже можно компоновать, взгляните: ```haskell {.example_for_playground .example_for_playground_003} 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 ``` Здесь `IPv4 $ "173.194.122.194"` — значение типа `IPAddress`. Вам это ничего не напоминает? Это же в точности так, как мы работали с функциями! Из этого мы делаем вывод: конструктор значения можно рассматривать как особую функцию. В самом деле: ```haskell EndPoint (IPv4 "173.194.122.194") 80 ``` Здесь `EndPoint` — "функция", `IPv4 "173.194.122.194"` — её первый аргумент, а `80` — второй аргумент. Мы как бы применяем конструктор к конкретным значениям как к аргументам, в результате чего получаем значение нашего типа. А раз так, мы можем компоновать конструкторы так же, как и обычные функции, лишь бы их типы были комбинируемыми. В данном случае всё в порядке: тип значения, возвращаемого конструктором `IPv4`, совпадает с типом первого аргумента конструктора `EndPoint`. Допустим, мы пишем движок для построения маршрутов. {.task_text} Заведите тип-перечисление `NaviType`, состоящий из двух значений: `Car` и `Bycicle`. Чтобы преобразовывать значение этого типа в строку, воспользуйтесь директивой `deriving`. {.task_text} Заведите тип `Loc`, обозначающий локацию: у него должен быть конструктор от кортежа из двух `Double` (широты и долготы). Создайте для него кастомную функцию `showLoc`, выводящую локацию в виде `"{широта; долгота}"`. {.task_text} И, наконец, заведите АТД `Route` (маршрут) с конструктором от `NaviType` (вид передвижения) и `Loc` (точка отправления). Для него также нужна функция `showRoute`. Внутри себя она должна применять `showNaviType` и `showLoc` и возвращать строку вида: {.task_text} `"Route from point {широта; долгота} by вид передвижения"` {.task_text} В функции `main` в блоке `where` проинициализируйте `route` маршрутом с видом передвижения `Bicycle` и отправной точкой `(56.981, 43.12)`. {.task_text} ```haskell {.task_source #haskell_chapter_0200_task_0010} module Main where -- Your code here main :: IO() main = print $ showRoute route where route = -- Initialize 'route' with values: -- NaviType=Bicycle, Loc=(56.981, 43.12) ``` Самое сложное в этой задаче — правильно расставить скобки. Для этого еще раз перечитайте примеры работы с АДТ в этой главе. {.task_hint} ```haskell {.task_answer} module Main where data NaviType = Car | Bicycle deriving (Show) data Loc = Loc (Double, Double) data Route = Route NaviType Loc showLoc :: Loc -> String showLoc (Loc loc) = "{" ++ (show (fst loc)) ++ "; " ++ (show (snd loc)) ++ "}" showRoute :: Route -> String showRoute (Route naviType start) = "Route from point " ++ showLoc start ++ " by " ++ show naviType main :: IO() main = print $ showRoute route where route = Route Bicycle (Loc (56.981, 43.12)) ``` ## Тип Сумма и Тип Произведение Почему алгебраические типы данных называются алгебраическими? Все просто: несколько типов компонуются, чтобы получить новый составной тип. При этом новый тип получается с помощью таких операций как сложение или умножение. А алгебра как раз-таки и работает со множеством сущностей и операций над ними. Но что значат сумма и произведение применительно к типам? У каждого типа есть свой допустимый набор значений. Количество таких значений называется **мощностью** типа. Например, значения типа `Bool` — это `True` либо `False`. Значит, мощность типа `Bool` равна 2. А сколько значений может принимать тип `LaptopState`, составленный из трех флагов, означающих, включен ли ноутбук, подключен ли он к электричеству и подключен ли по оптоволокну к сети? ```haskell data LaptopState = LaptopState Bool Bool Bool ``` Тип `LaptopState` имеет мощность 2 * 2 * 2, то есть 8. Мы получили 8, **перемножив** количество значений каждого из типов, составляющих `LaptopState`. Поэтому мы в праве сказать, что `LaptopState` — это Тип Произведение (Product Type). Также существует и Тип Сумма (Sum Type). Как нетрудно догадаться, мощность значений такого типа определяется через **сумму,** а не через произведение. Это тип, значения которого могут принимать один из нескольких взаимоисключающих вариантов. Взглянем на определение типа `Bool`: ```haskell data Bool = False | True ``` Как видите, `False` и `True` — это два взаимоисключающих варианта. Поэтому `Bool` — это Тип Сумма. Перед вами два пользовательских типа: `NaviType` и `Loc`. Напишите два разделенных пробелом значения для `NaviType` и `Loc`: {.task_text} - `sum`, если это Тип Сумма. - `product`, если это Тип Произведение. {.task_text} ```haskell data NaviType = Car | Bicycle data Loc = Loc (Double, Double) ``` ```consoleoutput {.task_source #haskell_chapter_0200_task_0020} ``` Значения типа `NaviType` могут принимать один из двух взаимоисключающих вариантов. А мощность типа `Loc` равна произведению всех значений, принимаемых `Double`, на 2. {.task_hint} ```haskell {.task_answer} sum product ``` Вот мы и познакомились с настоящими типами. Пришло время узнать о более удобной работе с полями типов.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!