← На главную

Как пекут хлеб программисты на Haskell

Недавно на Хабре появилась любопытная заметка Как два программиста хлеб пекли. В ней автор противопоставляет подход «сделай все с кучей классов и паттернами проектирования» подходу «сделай как можно проще (KISS)». А вот интересно, что будет, если попытаться решить описанную в статье задачу с помощью функционального подхода?

Итак, сидит себе такой программист на Haskell. Тут к нему подходит менеджер и заявляет, дескать, нам нужно научиться печь хлеб. В общем, ничего толком не ясно, но раз надо, так надо:

data Bread = Bread createBread = Bread

Позже становится известно, что хлеб, оказывается, должен печься в печке:

data Oven = Oven data Bread = Bread createBread :: Oven -> Bread createBread _ = Bread

Как видите, пока это мало на что влияет. Как и то, что печи бывают разных видов:

data Oven = ElectricOven | GasOven | MicrowaveOven data Bread = Bread createBread :: Oven -> Bread createBread _ = Bread

Первое действительно более-менее интересное условие состоит в том, что газовая печь не может печь без газа:

data GasStatus = GasAvailable | GasUnavailable data Oven = ElectricOven | GasOven | MicrowaveOven data Bread = Bread breadCouldBeCreated GasOven GasUnavailable = False breadCouldBeCreated _ _ = True createBread oven gas | breadCouldBeCreated oven gas = Just Bread | otherwise = Nothing

Здесь вводится тип «состояние газа». Газ – он либо есть, либо его нет. Если мы используем газовые баллоны, можно хранить количество доступного газа в литрах, суть от этого не меняется. Функция breadCouldBeCreated проверяет, можем ли мы что-нибудь приготовить при текущих обстоятельствах (наличие газа и тип печи).

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

data Stuffing = Meat | Cabbage data Food = Cake | Bread | Pasty Stuffing data GasStatus = GasAvailable | GasUnavailable data Oven = ElectricOven | GasOven | MicrowaveOven ovenCouldBeUsed GasOven GasUnavailable = False ovenCouldBeUsed _ _ = True create food oven gas | ovenCouldBeUsed oven gas = Just food | otherwise = Nothing

Вводим типы «еда» и «начинка». Функцию breadCouldBeCreated переименовываем в ovenCouldBeUsed.

Теперь менеджер хочет, чтобы торты, пирожки и хлеб пеклись не просто так, а по разным рецептам. Сказано – сделано:

data Stuffing = Meat | Cabbage data Food = Cake | Bread | Pasty Stuffing data GasStatus = GasAvailable | GasUnavailable data Oven = ElectricOven | GasOven | MicrowaveOven ovenCouldBeUsed GasOven GasUnavailable = False ovenCouldBeUsed _ _ = True create food oven gas | ovenCouldBeUsed oven gas = Just food | otherwise = Nothing breadRecipe = create Bread cakeRecipe = create Cake pastyRecipe stuffing = create $ Pasty stuffing

Рецепт обычно предписывает выполнение каких-то действий с ингредиентами, печью и так далее. Очевидно, что рецепт представляет собой функцию высшего порядка.

Наконец, в печах требуется обжигать кирпичи. Судя по формулировке, обжигать кирпичи следует в тех же самых печах, включая микроволновую (хотя в статье на Хабре вводится отдельный класс Furnace):

data Stuffing = Meat | Cabbage data Food = Cake | Bread | Pasty Stuffing data GasStatus = GasAvailable | GasUnavailable data Oven = ElectricOven | GasOven | MicrowaveOven data Brick = Brick ovenCouldBeUsed GasOven GasUnavailable = False ovenCouldBeUsed _ _ = True create food oven gas | ovenCouldBeUsed oven gas = Just food | otherwise = Nothing breadRecipe = create Bread cakeRecipe = create Cake pastyRecipe stuffing = create $ Pasty stuffing makeBrick oven gas | ovenCouldBeUsed oven gas = Just Brick | otherwise = Nothing

Здесь мы просто добавили тип «кирпич» и функцию «сделать кирпич».

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

Возможно, в действительности имелось в виду, что каждая печь производит хлеба и торты несколько иначе, и нам понадобится ввести дополнительный класс типов (или, если хотите, «интерфейс») с соответствующими экземплярами классов. Заодно нам не придется вручную кодировать вызов ovenCouldBeUsed всюду, где используется печь. Но не похоже, что решение существенно усложнится от всего этого.

А как вы печете хлеб?