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

4 октября 2012

Недавно на Хабре появилась любопытная заметка Как два программиста хлеб пекли. В ней автор противопоставляет подход «сделай все с кучей классов и паттернами проектирования» подходу «сделай как можно проще (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 всюду, где используется печь. Но не похоже, что решение существенно усложнится от всего этого.

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

Кстати, пользуясь случаем, вновь призываю вас поддержать нашу кампанию на BoomStarter по сбору средств на запись второго сезона EaxCast. Поддержать можно как рублем, так и рассказав о кампании друзьям. Помогите нам сделать хороший, годный программерский подкаст!

Метки: , .

Подпишитесь на блог с помощью RSS, E-Mail, Google+ или Twitter.

Понравился пост? Поделитесь с другими: