Здорово, когда на работе можно заниматься не только важными и серьёзными делами, но и чем-то интересным, пусть и без явной пользы. Наши деврелы активно поддерживают эту позицию и время от времени подкидывают идеи каких-нибудь забавных штук. В этот раз было так: «А давайте возьмём рецепты новогодних салатов и напишем их кодом?».
«А давайте», — подумал я. У нас как раз код-фриз и заниматься продуктовыми задачами нельзя. А окунуться в новогоднее настроение — можно :)
Началось всё с того, что я попробовал закодить рецепт оливье в виде класса на Kotlin, которому на вход единственного метода приходят «ингредиенты», а выдаёт он готовый салат. Шло неплохо, но получалась простая трансляция рецепта на язык программирования — код практически один в один повторял шаги приготовления, только синтаксис был другой. Скукотища.
Смотрите сами, вот этот рецепт:
ИНГРЕДИЕНТЫ (на 4 новогодних порции)
- 3 картофелины среднего размера
- 3-4 моркови
- 4-5 яиц
- 450 г консервированного зеленого горошка
- 5-6 солёных огурцов
- 350 г колбасы, ветчины отварной говядины или куриной грудки (выбирай на свой вкус)
- 5 побегов зеленого лука
- 1 пучок укропа
- 250 г майонеза
- соль
- чёрный молотый перецПОШАГОВЫЙ РЕЦЕПТ ПРИГОТОВЛЕНИЯ
1. Возьмите двухлитровую кастрюлю, наполните водой до середины, поставьте нагреваться на плиту.
2. Пока вода греется, подготовьте овощи. Картофель и морковь тщательно вымойте щёткой.
3. Когда вода в кастрюле закипит, добавьте половину чайной ложки соли, положите овощи. Варите до готовности овощей.
4. Вымойте яйца, возьмите кастрюлю подходящего размера, положите внутрь яйыа и залейте водой так, чтобы она покрывала их полностью. Поставьте кастрюлю на плиту, варите до готовности, не допускайте переваривания желтка (5-7 минут после закипания).
5. Овощи и яйца остудите, очистите и нарежьте кубиками со стороной 5 мм. Такими же кубиками нарежьте ветчину или колбасу, солёные огурцы.
6. Откройте банку консервированного горошка, слейте жидкость.
7. Вымойте, высушите и мелко нарежьте зелёный лук и укроп.
8. Все ингредиенты смешайте в большом салатнике, добавьте соль по вкусу, приправьте чёрным молотым перцем.
9. Перемешайте, заправьте майонезом, поставьте в холодильник.Если предпочитаете салат с говядиной или курицей, то нужно заранее отварить их.
Похоже на код, не правда ли? Сделайте это, потом вот это, а здесь нужно делать действие, пока ждём результата предыдущего действия. Разве что «переменные» описаны перед «кодом», а не объявляются по необходимости, но и такое можно встретить в других языках программирования (например, Pascal). Ты, как какой-нибудь интерпретатор, исполняешь всё по этой инструкции и получаешь тазик оливье. Неудивительно, что идея написать это в императивном стиле пришла первой.
В реализации класса мне хотелось обойтись без подробностей готовки, поэтому я вводил абстракции налево и направо, в духе:
val boilEggs: (List<Egg>) -> List<BoiledEgg>
Ну не описывать же в деталях процесс варки яиц, в самом деле. Так и до салата можно не добраться. Код всё больше обрастал такими абстракциями: смешивание ингредиентов в салатнике, мытье картошки щёткой — это всё, на мой взгляд, было абсолютно неважно для готового блюда. Их создание занимало много времени, поэтому после примерно пары часов работы у меня было описано только три пункта из рецепта.
Тогда я подумал, как избавиться от них? Почти сразу вспомнил о декларативном стиле, который сейчас набирает популярность в мире мобильной разработки. Уже полгода как вышли стабильные версии UI-фреймворков Jetpack Compose и Swift UI, а на Flutter так и вообще уже написано немало приложений.
Их прелесть (как и вообще декларативного стиля) в том, что можно не задумываться о деталях реализации каждого элемента интерфейса и как добавить его на экран, а сконцентрироваться на том, что пользователь видит на экране своего телефона. Т.е. вместо описания последовательности действий мы в коде описываем желаемый результат:
-
не «добавить кнопку и установить ей красный цвет»,
val button = Button() button.setColor(Color.Red) addView(button)
-
а «красная кнопка»
Button(Color.Red)
Звучит здорово и как раз подходит для решения моей проблемы. Поэтому я решил переписать рецепт в декларативном стиле.
Получилось вот так (синтаксис по мотивам Jetpack Compose):
СалатОливье {
Смесь(
заправка = Майонез,
специи = [
Соль(количество = поВкусу),
ЧерныйПерец(вид = молотый, количество = поВкусу)
]
) {
Картофель(
обработка = [чистый, без(Кожица), отваренный],
нарезка = кубиками(5мм)
)
Морковь(
обработка = [чистый, без(Кожица), отваренный],
нарезка = кубиками(5мм)
)
Огурец(
обработка = [засоленный],
нарезка = кубиками(5мм)
)
Яйцо(
обработка = [чистый, без(Скорлупа), вареный(вкрутую)],
нарезка = кубиками(5мм)
)
Горох(
обработка = [консервированный]
)
НаВыбор {
Колбаса(
обработка = [вареная],
нарезка = кубиками(5мм)
)
Курица(
обработка = [вареная],
нарезка = кубиками(5мм)
)
Говядина(
обработка = [вареная],
нарезка = кубиками(5мм)
)
}
ЗеленыйЛук(нарезка = шинковка(мелкая))
Укроп(нарезка = шинковка(мелкая))
}
}
Что поменяется? Можно ли всё ещё рассчитывать на оливье?
Кажется, что с «декларативным рецептом» справится любой, даже начинающий повар. Разве что ребенку без базовых знаний будет сложно. А вот опытный шеф может просмотреть весь рецепт и легко поменять что-то по своему усмотрению, чтобы стало ещё лучше. Прямо как оптимизирующий компилятор. ;)
Какой подход использовать в своих «рецептах» — решать вам. А если декларативный вариант собьёт вас с толку, то берите бутылку шампанского и езжайте к маме — у неё точно найдётся, чем вас накормить.
И продолжайте экспериментировать! С Новым годом!
Комментарии (13)
fo2rist
30.12.2021 21:12+2Сначала императивный салат, а потом надо добавить маленькую фичу — "дать отстояться после заливки майонезом", и вот уже у нас все обложено `useEffect` хуками, и снова получается... спагетти! :)
OneeL Автор
30.12.2021 21:15+1А еще remember, remember не забыть! Ведь состояние - это неотъемлимая часть декларативного подхода.
mSnus
30.12.2021 22:12+1Мне кажется, формат yaml отлично бы подошёл. Простые, удобный и с майонезом хорошо сочетается.
OneeL Автор
30.12.2021 22:46Согласен. Если уж умный дом на yaml натянули (привет, Home Assistant!), то и с рецептами проблем не будет. А если все-таки понадобится немного императивщины, то можно Jinja2 использовать.
vics001
31.12.2021 05:45+2Эх, забываем мы про старые добрые декларативные языки программирования - Пролог, например :-)
Настоящее декларативное программирование, это не описание конечного состояния (декларативное описание), потому что для этого потребуется некоторая специфическая машина, которая будет знать эти состояния. В общем виде декларативный стиль будет отличаться для салата отсутствием шагов и "программа" должна сама правильно определить шаги и параллелизм, но все равно будет подозрительно похож,.
готовый_салат(Салат) :- заготовка(Заготовка), заправить_майонез(Заготовка, Результат), поставить_холодильник(Результат, Салат). заготовка(Заготовка) :- подготовить(Яйца), подготовить(Салат), ..., все_смешать([Яйца, Салат, ... ], Рез-т), добавить(Рез-т, Соль, 5г, Рез-т-2), добавить(Рез-т, Перец, 5г, Заготовка). // .... // и т.д. до самых примитивов
OneeL Автор
31.12.2021 16:38Меня эта тема еще с курса в институте интересовала - хотелось понять область применения таких языков. Рад, что мы нашли где они могут быть полезны :D
akaDuality
31.12.2021 10:28Cooklang — язык для описания рецептов
OneeL Автор
31.12.2021 16:34Я с первого раза не смог понять - это у ребят так шутка затянулась, что они и язык, и приложение, и, даже, интерфейс командной строки сделали, или это прям серьезный набор инструментов.
Задумки у нас разные, но классно, что мысли сходятся :)
georgepolevoy
31.12.2021 20:32Мне этот пост интересен, потому что рецепт это метафора, которую я очень люблю приводить в качестве объяснения принципа работы процессора и стека новичкам в программировании.
Повар - процессор. Разделочная доска - регистры процессора. Рецепт - программа... ну и в таком духе.
И вот к сожалению декларативный стиль программы тут совсем не катит. Ну или по крайней мере - тут не хватает концепции хронометрирования, и нужен компилятор в уже нормальный рецепт, который может исполнить процессор (повар).
В оригинальном рецепте, который вы приводите, есть "Пока вода греется", "Охладите овощи". Если вы думаете, что достаточно просто овощи в любой момент на любое время снять с огня - ошибаетесь.
Винегрет это не бог весть что, но представьте себе хотя бы овощи аль денте или стейк, где дорога каждая секунда, я уже не говорю о чем-то более сложном. Вы пассируете там что-то со специями 30 секунд, а потом засыпаете рис (который в этот момент как раз дошел до кондиции) и тут вы понимаете, что рис вам еще предстоит только начать варить, вариться он будет 6 минут, и к тому времени овощи в пассировке превратятся в кашу (они горячие, и процесс готовки не останавливается с выключением огня - нужно очень быстро все охладить), и чтобы этого не произошло вам нужно выкинуть все в помойку, разобрать ваш декларативный рецепт, прохронометрировать его, превратить в императивный, и тогда уже приступать заново, чтобы все секунда в секунду сварилось и пассировалось, прежде чем охладить овощи...
Человек до мозга костей императивен, и ему как раз очень удобно получать пошаговые инструкции, именно в таком виде, в котором, обычно, публикуют рецепты.
Кстати, если вы решились готовить по рецепту из интернета, потрудитесь подготовиться как следует, проанализировать все фазы готовки, чтобы для вас посреди рецепта не стало неожиданностью, что вам не хватает ковша или сковородки. Чтобы получилось вкусно, нужно досконально следовать рецепту с секундомером наготове, со всеми нужными приборами, конечно, если только вы не гений кухни, но тогда вам не нужен рецепт.
Все это наводит на мысль о том, что нужно анализировать предметную область вместе с экспертами, и у них не случайно свой DSL.
Удачной готовки!
OlegZH
31.12.2021 20:57А что делать, если на одни продукты выдаёт исключения желудочно-кишечный тракт, на другие — сердечно-сосудистая система, всякие приправы вызывают переполнение буфера, а алкоголь — нарушение общей защиты (GPF)?
corporateanon
Декларативный стиль хорош для салатов, где процессы легко параллелятся и нет большого количества состояний. А получится ли, например, формализировать борщ без императивщины?
OneeL Автор
Мне кажется, это возможно, но может потребоваться ввести более высокоуровневые абстракции вроде Бульона и Зажарки :)