понедельник, 22 сентября 2014 г.

Декомпозиция в ФП vs декомпозиция в императивном программировании

Любой программист разбивает программу на модули, модули на файлы, файлы на классы, классы на функции (если язык не объектно-ориентированный, то файлы сразу на функции). В среде императивных программистов есть устоявшийся набор правил, по которому эту декомпозицию надо делать. В основном, люди отталкиваются от принципов SOLID, KISS, DRY, Tell Don't Ask, закона Деметры, шаблонов проектирования, правил вроде "функция должна умещаться в экран" и т.п.
Забавно, но нигде из вышеупомянутых правил вообще нет никакого упоминания о side-эффектах. Схематично, программа, написанная в императивном стиле, выглядит примерно вот так:

Голубые прямоугольники ‒ это функции/классы, красные ‒ это строки с side-эффектами, желтые ‒ строки без side-эффектов.
Всё вроде бы хорошо, однако до тех пор, пока вам не приходится эти функции компоновать, т.е. объединять вызовы функций в более высокоуровневые функции, чтобы получилась программа. Часто бывает, что одна и та же функция может быть вызвана более 1 раза в проекте.
Почему трудно компоновать функции с side-эффектами? Потому что объединяя несколько функций в одну, вам нужно держать в голове все side-эффекты, которые каждая из функций содержит, и всё время анализировать: "устраивает ли меня, что серия вызовов вот этих функций кроме того что делает основную работу, ещё имеет следующий набор side-эффектов?"
Очень часто вот этот итоговый набор side-эффектов нежелателен. К примеру, функция f осуществляет запуск ракеты в космос и возвращает массу ракеты. Если вам нужно в проекте в 10 местах узнать массу ракеты, но запуск ракеты вам не нужен, то функция f для вас бесполезна! Вы её не сможете переиспользовать! Вы ведь не хотите, чтобы ракета запустилась тогда, когда пользователь запустил вашу программу, просто чтобы посмотреть характеристики этой ракеты?!
Что нужно сделать с функцией f(), чтобы её можно было использовать не только в случае реальной необходимости запуска ракеты, но и в других местах? Нужно выделить часть, отвечающую за запуск (side-эффект), в отдельную функцию, а часть, отвечающую за расчет массы ракеты, ‒ в другую функцию. Таким образом, вы отделяете чистые функции от функций с side-эффектами:

Вот эта желтая часть должна содержать большую часть вашей бизнес-логики и составляет где-то 70-80 процентов от всего проекта.
Что самое интересное, никто не мешает вам совмещать вот такое разделение с использованием вышеперечисленных привычных вам правил и законов. Хотите применять ООП, наследование и паттерны GoF (если вам так нравится) ‒ применяйте! Только отделяйте side-эффекты от чистых функций! Несколько паттернов, конечно, придется немного подкорректировать. Вот здесь описан, например, чистофункциональный визитор.
Выделение чистых функций, конечно, даётся не бесплатно. Но время, потраченное, на такую правильную декомпозицию, с лихвой окупается, когда дело доходит до компоновки и переиспользования функций! Чистые функции можно переиспользовать как угодно и сколько угодно, не задумываясь! Вы смотрите только на входные аргументы и на результат функции. Кроме того, чистые функции очень легко тестировать (любители TDD оценят).

4 комментария:

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

    ОтветитьУдалить
    Ответы
    1. Юра, какой то поток сознания. Ничего не понял.

      Удалить
  2. Извини. Хотел покороче написать.

    Проблема которую ты описываешь решается в обычном псевдо-ООП (типа ц++) несколькими разными способами. В частности, в тех наборах принципов что ты упомянул типа SOLID и прочих специально есть принципы для этого. Например, разделение обязанностей. В твоём примере с ракетой, например, возвращать массу ракеты будет один класс (Ракета или даже специальный отличный от самого класса Ракета), а запускать её совсем другой. Был бы пример более правдоподобный, можно было бы обсудить в деталях.

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

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

    Суммируя, по-моему твоя статья о функциональной декомпозиции, хотя озаглавлена она как сравнение функциональной с императивной. Ты не рассказал ничего на самом деле об императивной. Кроме того, даже если бы рассказал, то увидел бы сам, что твоя пропаганда композиции функций в императивной парадигме вообще не используется практически. Так что твой аргумент что чистые функции "окупятся с лихвой" когда начнём композировать, ложен для сред в которых есть что-то кроме чистой функциональщины. В таких средах не окупятся чистые функции, а окупятся совсем другие приёмы и методы.

    ОтветитьУдалить
    Ответы
    1. >> В твоём примере с ракетой, например, возвращать массу ракеты будет один класс (Ракета или даже специальный отличный от самого класса Ракета), а запускать её совсем другой.
      Я не понял, это возражение или нет? Все правильно: логика вычисления массы ракеты надо отделять от внешнего эффекта запуска ракеты. Я это и сказал.

      >> Был бы пример более правдоподобный, можно было бы обсудить в деталях.
      Не знаю, чем тебе не нравится пример. Ну давай другой возьмем (хотя сути это абсолютно не поменяет): кто-то написал функцию f, читающую текстовый файл и осуществляющую некую сложную логику над полученной строкой (например, найти сколько раз встречается определённая буква в нём). Функция f будет бесполезна, когда понадобится выполнить эту сложную логику над строкой, полученной не из файла, а например, из html-страницы из Интернета.

      >> Но если мы выйдем за пределы чистой функциональщины и обогатим ящик с инструментами ООП и прочими штуками, то окажется, что чистые функции уже не так сильно и важны.
      Проходили уже это. ООП не спасает, сложность растет с ростом размера проекта. Нужна более строгая культура (ФП).

      >> И цена их сильно выше чем то что они дают в большинстве случаев в жизни программиста.
      А кто сказал, что будет легко? К тому же тяжело только по началу. После нескольких месяцев работы над проектом, где доминирует ФП-парадигма, человек осваивается и начинает без особого труда писать в ФП-стиле.

      >> Ты не рассказал ничего на самом деле об императивной
      Я и не ставил такой задачи. Про императивную декомпозицию книг вагон и маленькая тележка. GoF, Макконнелл, Дядя Боб - там всё это описано. Зачем мне это повторять? Но все рецепты в этих книгах не решают проблемы увеличения сложности. Это проверено на 3 крупных проектах (> 500 000 LOC), в которых я участвовал. Из императивной декомпозиции мне лишь нужно было упомянуть, что в итоге просто получаются функции с side-эффектами.

      >> ... ложен для сред в которых есть что-то кроме чистой функциональщины
      Каких таких сред? Функциональщина - это способ решения задачи, а не характеристика среды.

      Удалить