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

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

EnglishRussianUkrainian