Семьи типов и Покемоны

от автора

Предисловие

Когда я начал изучать Хаскель, я был почти сразу поражён. Для начала, нырнув с головой в актуальные рабочие проекты, открыл, что большинство настоящих библиотек используют языковые расширения, присутствующие только в GHC (Glasgow Haskell Compiler). Это меня покоробило слегка, прежде всего потому, кто захочет использовать язык настолько немощный, что будет необходимо использовать расширения, присутствующие лишь у одного поставщика. Ведь так?
Хорошо, я решился снова это осилить и узнать всё об этих расширениях, и я вывел три горячих топика для общества Хаскеля, которые решали похожие проблемы: Обобщённые Алгебраические Типы Данных, Семьи Типов и Функциональные Зависимости. Пытаясь найти ресурсы, которые обучают о них, я смог найти только статьи, описывающие, что это такое, и как их использовать. Но никто, на самом деле, не объяснял зачем они нужны!.. Поэтому я решил написать эту статью, используя дружественный пример, пытаясь объяснить зачем всё-таки нужны Семьи Типов.

Вы когда-нибудь слышали про Покемонов? Это замечательные существа, которые населяют Мир Покемонов. Вы можете считать, что они как животные с экстраординарными способностями. Все покемоны владеют стихией, и все их возможности зависят от этой стихии. Например, покемон Огненной стихии может дышать огнём, в то время как покемон Водной стихии может брызгать струями воды.
Покемоны принадлежат людям, и их специальные способности могут быть использованы во благо для продуктивной деятельности, но некоторые люди всего лишь используют своих покемонов для борьбы с другими покемонами других людей. Эти люди называют себя Тренерами Покемонов. Это может сначала звучать как жестокое обращение с животными, но это очень даже весело и все, похоже, рады, включая покемонов. Имейте в виду, что в мире покемонов, кажется, всё в порядке, даже если 10-летние покидают дом, дабы рисковать своими жизнями ради того, чтобы стать самыми лучшими Тренерами Покемонов, как будто никто и никогда таковыми не становился.

Мы собираемся использовать Хаскель для того, что бы представить ограниченную (и даже несколько упрощённую, да простят меня фанаты) часть мира покемонов. А именно:

  • Покемон имеет тип или стихию, в нашем случае урезанную до Огня, Воды и Травы
  • Существуют три покемона каждой стихии:
    • Чармандер, Чармелион и Чаризард — Огненные покемоны,
    • Сквиртл, Вартортл и Блестойз — Водной стихии,
    • Бульбазавр, Ивизавр и Венозавр — Травяной стихии
  • Каждая стихия имеет свои собственные способности, называемые движениями или ударами: водные покемоны выполняют водные удары, огненные покемоны — огненные удары, травяные — травяные удары
  • Когда бьются, огненный покемон всегда побеждает травяного покемона, травяной покемон всегда побеждает водного покемона, а водный покемон всегда побеждает огненного покемона
  • Никогда не дерутся покемоны одной стихии, поскольку нельзя определить, кто из них выиграет
  • Другие люди должны быть способны пополнять программу своими покемонами в своих модулях
  • Проверщик Типов (часть интерпретатора и компилятора) должен помочь нам в соблюдении правил

Первая попытка

Для начала попытаемся реализовать правила без использования классов типов и семей типов.
Начнём с нескольких стихий покемонов и их движений. Мы будем реализовывать отдельно, поскольку это поможет нам отличить их движения от типов покемонов.
Для этой цели мы определим функции для каждого покемона, выбрав его движение.

data Fire = Charmander | Charmeleon | Charizard deriving Show  data Water = Squirtle | Wartortle | Blastoise deriving Show data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show  data FireMove = Ember | FlameThrower | FireBlast deriving Show  data WaterMove = Bubble | WaterGun deriving Show data GrassMove = VineWhip deriving Show  pickFireMove :: Fire -> FireMove pickFireMove Charmander = Ember pickFireMove Charmeleon = FlameThrower pickFireMove Charizard = FireBlast  pickWaterMove :: Water -> WaterMove pickWaterMove Squirtle = Bubble pickWaterMove _ = WaterGun  pickGrassMove :: Grass -> GrassMove pickGrassMove _ = VineWhip 

Пока всё хорошо, проверщик типов помогает разобраться, где какой покемон правильно использует свою стихию.


6 из описываемых нами 9 покемонов всех трёх стихий. По 2 на каждый тип

Теперь мы должны реализовать бой. Бои будут представлять собой вывод сообщения, где описано как каждый покемон бьёт, затем указываем победителя, например, так:

printBattle :: String -> String -> String -> String -> String -> IO () printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do   putStrLn $ pokemonOne ++ " used " ++ moveOne   putStrLn $ pokemonTwo ++ " used " ++ moveTwo   putStrLn $ "Winner is: " ++ winner ++ "\n" 

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

battleWaterVsFire :: Water -> Fire -> IO () battleWaterVsFire water fire = do   printBattle (show water) moveOne (show fire) moveTwo (show water)  where   moveOne = show $ pickWaterMove water   moveTwo = show $ pickFireMove fire  battleFireVsWater = flip battleWaterVsFire -- То же самое, что и выше, только с аргументами, которых поменяли местами 

Если всё это объединим, и допишем другие функции драк, мы получим программу.

Введение в Классы типов

Сколько в этом повторенного кода: представьте себе, что кто-то захотел добавить Электрической стихии покемонов, например Пикачу, тогда придётся дописывать собственные функции драк battleElectricVs(Grass|Fire|Water). Есть несколько шаблонов, которые помогут нам формализовать и помочь людям получить большее понимание, что такое покемоны и как добавлять новые.
Что мы имеем:

  • покемоны используют функции, чтобы выбрать движение
  • Битвы находят победителя и печатают описание битвы

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

Класс покемонов

Класс покемонов отображает знания, что покемон выбрал своё движение. Это позволит нам определить pickMove, пере-используя так, что одна и та же функция может оперировать разными стихиями, для которых определён класс.

В отличие от «ванильных» классов, наш класс покемонов будет нуждаться в 2х типах: стихии покемона и типа урона им используемым, а позже одно будет зависеть от другого. Мы должны включить языковое расширение, разрешающее иметь 2 параметра в классе: MultiParamTypeClasses
Заметьте, что мы должны добавить ограничения, такие, что покемоны и их удары должны иметь возможность быть выведенными на экран.
Вот определение, наряду с несколькими экземплярами для существующих стихий покемонов.

class (Show pokemon, Show move) => Pokemon pokemon move where   pickMove :: pokemon -> move  instance Pokemon Fire FireMove where   pickMove Charmander = Ember   pickMove Charmeleon = FlameThrower   pickMove Charizard = FireBlast  instance Pokemon Water WaterMove where   pickMove Squirtle = Bubble   pickMove _ = WaterGun  instance Pokemon Grass GrassMove where   pickMove _ = VineWhip 

и сможем использовать функцию так

pickMove Charmander :: FireMove 

Заметьте, как вещи начинают выглядеть неопрятно. Из-за того, что стихии покемонов и типы движения обрабатываются независимо классами типов. Говоря, что мы выбираем Огненный удар, мы даём проверщику типов всю информацию, для того, что бы он решил, какой использовать класс и удар.

Класс битвы

У нас уже есть покемоны, которые могут выбирать себе удары, теперь нам необходима абстракция, которая будет представлять битву двух покемонов, для того, что бы избавиться от функций типа battle*family*Vs*family
Нам бы очень хотелось написать код так:

class (Pokemon pokemon move, Pokemon foe foeMove) => Battle pokemon move foe foeMove where   battle :: pokemon -> foe -> IO ()   battle pokemon foe = do     printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)    where     move = pickMove pokemon     foeMove = pickMove foe  instance Battle Water WaterMove Fire FireMove 

Однако, если мы запустим, получим ошибку от проверщика типов, поскольку нет более общего экземпляра с учётом всех типов ударов.
Эта проблема решаема, однако, итоговый код выглядит некрасивым, нам фактически необходимо изменить тип функции битвы на

battle :: pokemon -> foe -> IO (move, foeMove) 

Введение Семей типов, наконец-то!

Ну вот, наша программа выглядит удручающе. Мы должны заботится обо всех подписях типов, и мы даже обязаны изменять внутреннее поведение наших функций (battle) только для того, чтобы мы могли использовать подписи типов для того, дабы помочь компилятору. Я могу пойти значительно дальше и сказать, что наш нынешний рефакторинг программы — лишь чуть более формальный и менее повторяемый, не настолько уж и большое достижение, после того, как мы ввели столько безобразия в код.
Теперь мы можем оглянутся назад, на наше определение класса Покемон. Он имеет стихию покемонов и тип ударов как два отдельных переменных класса. Проверщик типов не знает о существовании связи между стихиями покемонов и типами ударов. Он даже позволяет определить экземпляр Покемонов, когда Водный покемон создаёт Огненные удары!
Именно тут семьи типов вступают в игру: они позволяют сказать проверщику типов, что Огненный покемон может работать только с Огненными ударами и так далее.

Класс Покемон, используя семьи типов

Для того, что бы использовать Семьи типов нам необходимо включить расширение TypeFamilies. Как только мы подключим, мы сможем попробовать написать наш класс в стиле:

{-# LANGUAGE TypeFamilies, FlexibleContexts #-} class (Show p, Show (Move p)) => Pokemon p where   data Move p :: *   pickMove :: p -> Move p 

Мы определили наш класс Покемон таким образом, что он имеет один аргумент и один ассоциированный тип Движения. Тип Движения становится «функцией типа», возвращающей тип удара, который будет использован. Это означает, что мы будем вместо FireMove использовать Move Fire, вместо WaterMoveMove Water и т.д.
Заметим, что зависимость выглядит почти как в предыдущем случае, только вместо Show move мы используем Show (Move a)). Нам необходимо включить ещё одно дополнение: FlexibleContexts, что бы работать с этим.
Теперь Хаскель обеспечивает нас отличным синтаксическим сахаром, поэтому мы можем определить актуальный ассоциированный конструктор данных справа, когда мы определяем наш экземпляр.
Давайте переопределим все наши типы данных и создадим необходимые экземпляры класса, используя семьи типов.

data Fire = Charmander | Charmeleon | Charizard deriving Show instance Pokemon Fire where   data Move Fire = Ember | FlameThrower | FireBlast deriving Show   pickMove Charmander = Ember   pickMove Charmeleon = FlameThrower   pickMove Charizard = FireBlast  data Water = Squirtle | Wartortle | Blastoise deriving Show instance Pokemon Water where   data Move Water = Bubble | WaterGun deriving Show   pickMove Squirtle = Bubble   pickMove _ = WaterGun  data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show instance Pokemon Grass where   data Move Grass = VineWhip deriving Show   pickMove _ = VineWhip 

Теперь мы можем спокойно писать

pickMove Squirtle 

и получить результат.
Это красиво, правда же? Нет больше необходимости писать подписи, для того, чтобы выбрать удар.
Однако ещё рано сравнивать с начальным вариантом. Лучше сравнить финальный результат, чтобы получить полный эффект от увиденного.

Новый класс Битвы

Теперь уже нет необходимости в длинной подписи, поэтому можно убрать отвратительный костыль, и вернуть почти первоначальное значение.

class (Pokemon pokemon, Pokemon foe) => Battle pokemon foe where   battle :: pokemon -> foe -> IO ()   battle pokemon foe = do     printBattle (show pokemon) (show move) (show foe) (show foeMove) (show pokemon)    where     foeMove = pickMove foe     move = pickMove pokemon 

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

instance Battle Water Fire instance Battle Fire Water where   battle = flip battle  instance Battle Grass Water instance Battle Water Grass where   battle = flip battle  instance Battle Fire Grass instance Battle Grass Fire where   battle = flip battle 

Использовние тоже просто:

  battle Squirtle Charmander 

Это всё! Наша программа наконец приобрела отличный вид, мы улучшили её, и проверщик типов проверяет больше, меньше повторяем и имеем чистую API для того, что бы предлагать её другим разработчикам.

Классно! Мы сделали это! Надеюсь, вам понравилось!
Ладно-ладно. Я понял, что вам весело и вы не можете поверить, что всё уже закончилось, потому что ваш скролбар в браузере показывает, что ещё есть место пониже этой фразы.

Что же, давайте добавим ещё одну вещь в Мир Покемонов.
Сейчас мы определили наши экземпляры Битвы для стихий Water и Fire как Battle Water Fire, и затем Battle Water Fire таким же самым как и предыдущий, с аргументами поменянными местами. Первый покемон всегда выигрывает, и выводится всегда следующее:

-- Winner Pokemon move -- Loser Pokemon move -- Winner pokemon Wins. 

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

-- Loser Pokemon move -- Winner Pokemon move -- Winner pokemon Wins 

Ассоциированные Синонимы типов

Когда мы решаем возвращать выбор двух типов, мы обычно используем Either a b, но это в ран-тайме, мы же хотим, что бы проверщик типов был уверен, что когда будут драться стихии Огонь и Вода, Вода будет всегда победителем.
Поэтому мы добавим новую функцию в Битву и назовём её победитель, которая будет получать 2 аргумента в том же самом прядке, которые были получены функцией битвы, и решим кто будет выигрывать.
Однако возвращать одно из нескольких вариантов вызывает неопределённость выбора подписи у победителя.

class Battle pokemon foe where .. winner :: pokemon -> foe -> ??? -- Так что же, 'pokemon' или 'foe'?  instance Battle Water Fire where   winner :: Water -> Fire -> Water -- Water первая переменная класса : pokemon   winner water _ = water  instance Battle Fire Water where   winner :: Fire -> Water -> Water -- Water вторая переменная класса: foe   winner _ water = water 

Видите, для Battle Water Fire экземпляра возвращается тип победителя такого же, как и pokemon, а у Battle Fire Water это уже будет foe.

К счастью, семьи типов так же поддерживают ассоциированные синонимы типов. В классе Битвы мы будет иметь Winner pokemon foo, а в экземплярах будем определять, кто же из них им будет. Мы используем тип, а не данные, потому что это всего лишь синоним pokemon или foe.
Самостоятельно Winner является функцией типа с подписью видов * -> * -> *, которая получает обоих pokemon и foo, и возвращает одного из них.
Мы так же определим реализацию по умолчанию, которая будет выбирать pokemon

class (Show (Winner pokemon foe), Pokemon pokemon, Pokemon foe) => Battle pokemon foe where   type Winner pokemon foe :: *      -- это ассоцированный тип   type Winner pokemon foe = pokemon -- это его имплементация по умолчанию    battle :: pokemon -> foe -> IO ()   battle pokemon foe = do     printBattle (show pokemon) (show move) (show foe) (show foeMove) (show winner)    where     foeMove = pickMove foe     move = pickMove pokemon     winner = pickWinner pokemon foe      pickWinner :: pokemon -> foe -> (Winner pokemon foe) 

Экземпляры создаются так:

instance Battle Water Fire where   pickWinner pokemon foe = pokemon  instance Battle Fire Water where   type Winner Fire Water = Water   pickWinner = flip pickWinner 

Теперь уж точно всё.
Окончательный вариант программы таков:

Окончательный вариант битв покемонов

{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, FlexibleContexts #-} class (Show pokemon, Show (Move pokemon)) => Pokemon pokemon where   data Move pokemon :: *   pickMove :: pokemon -> Move pokemon  data Fire = Charmander | Charmeleon | Charizard deriving Show instance Pokemon Fire where   data Move Fire = Ember | FlameThrower | FireBlast deriving Show   pickMove Charmander = Ember   pickMove Charmeleon = FlameThrower   pickMove Charizard = FireBlast  data Water = Squirtle | Wartortle | Blastoise deriving Show instance Pokemon Water where   data Move Water = Bubble | WaterGun deriving Show   pickMove Squirtle = Bubble   pickMove _ = WaterGun  data Grass = Bulbasaur | Ivysaur | Venusaur deriving Show instance Pokemon Grass where   data Move Grass = VineWhip deriving Show   pickMove _ = VineWhip  printBattle :: String -> String -> String -> String -> String -> IO () printBattle pokemonOne moveOne pokemonTwo moveTwo winner = do   putStrLn $ pokemonOne ++ " used " ++ moveOne   putStrLn $ pokemonTwo ++ " used " ++ moveTwo   putStrLn $ "Winner is: " ++ winner ++ "\n"  class (Show (Winner pokemon foe), Pokemon pokemon, Pokemon foe) => Battle pokemon foe where   type Winner pokemon foe :: *   type Winner pokemon foe = pokemon    battle :: pokemon -> foe -> IO ()   battle pokemon foe = do     printBattle (show pokemon) (show move) (show foe) (show foeMove) (show winner)    where     foeMove = pickMove foe     move = pickMove pokemon     winner = pickWinner pokemon foe      pickWinner :: pokemon -> foe -> (Winner pokemon foe)  instance Battle Water Fire where   pickWinner pokemon foe = pokemon  instance Battle Fire Water where   type Winner Fire Water = Water   pickWinner = flip pickWinner  instance Battle Grass Water where   pickWinner pokemon foe = pokemon  instance Battle Water Grass where   type Winner Water Grass = Grass   pickWinner = flip pickWinner  instance Battle Fire Grass where   pickWinner pokemon foe = pokemon  instance Battle Grass Fire where   type Winner Grass Fire = Fire   pickWinner = flip pickWinner      main :: IO () main = do   battle Squirtle Charmander   battle Charmeleon Wartortle   battle Bulbasaur Blastoise   battle Wartortle Ivysaur   battle Charmeleon Ivysaur   battle Venusaur Charizard 

Теперь можно своего Электрического покемона очень просто добавить! Попробуйте!

P.S. Оригинал статьи Type Families and Pokemon
Статья печатается с сокращениями, поскольку предназначена для интерактивного взаимодействия.

ссылка на оригинал статьи http://habrahabr.ru/post/187272/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *