Comments 320
Хаскель, будучи языком потрясающе аккуратным и красивым языком, при этом совершенно не подходит для задач, реализуемых не докторами наук. К сожалению, простые задачи на нем очень быстро набирают высокую сложность и требуют ощутимо больших трудозатрат, чем практически все другие функциональные языки, и дело здесь совершенно не в наличии или отсутствии библиотек, а в необходимости тщательного проектирования взаимодействия сайдэффектов и чистого кода, что хорошо подходит для академического программирования и совершенно не работает с промышленным.
Хаскель подходит для промышленных задач, и люди, которые его используют именно так, очень недоумевают по поводу мифов, в том числе и приведенных вами.
www.haskell.org/onlinereport/preface-jfp.html
The committee's primary goal was to design a language that satisfied these constraints:
It should be suitable for teaching, research, and applications, including building large systems.
Для немонадических «маленьких частей» все вообще очевидно. Композиция функций с возможным добавлением прослоек с преобразованиями типов.
С монадическими или стрелочными «частями» надо смотреть. Если монады не покидают границ модуля, как в первом случае. Если это простая монада, которую можно «запустить» (типа парсера), тоже обычно все просто. Если там IO или STM, то применять их можно только в другом монадическом коде — но это получается мало отличается от обычного императивного программирования.
Искать эту проблему бывает не просто, но возникает она когда все остальное уже работает.
А вот привычка к ленивости при переходе на C++ и Scala принесла необходимость отлаживать инициализацию взаимозависимых объектов. В ленивом языке с этим было проще.
ФП всего лишь говорит вам — не хотите неопределенного поведения — уберите сайд-эффекты. Это не значит, что их не должно быть, просто они должны жить отдельно. Отделяйте операции над данными от самих данных. Я для себя выработал следующий подход: Обработка данных — класс, который через конструктор принимает объект с состоянием.
class NumericOperation {
NumberState state;
NumericOperation(this.state);
add() {
return state.a + state.b;
}
}
class NumberState {
int a;
int b;
}
Таким образом все состояние над которым мы производим операции можно протестировать, потому что мы его задаем только в конструкторе. Легко пользоваться моками. И да, несмотря на то, что NumericOperation хранит ссылку на состояние это не мешает нам писать в ФП стиле. Ведь функция add — чистая.
Для того, чтобы поддерживать систему и дальше. даже если она будет очень сложной — используйте Dependency Injection. Таким образом вы не будете сами инстанцировать классы, вы будете только указывать каким образом это сделать и какие зависимости должны быть прокинуты. Так можно расширять контракт без потери обратной совместимости на уровне входных данных. Мы не должны зависеть от атомарных типов. Мы должны зависеть от абстракций.
class NumericOperation {
NumberState state;
StringState stringState;
NumericOperation(this.state, this.stringState);
add() {
return state.a + state.b + parseToInt(stringState.c);
}
parseToInt(String c) {
// логика парсинга
return someNumber;
}
}
Ведь за проброску данных отвечает DI и нам достаточно поправить вызов DI, а не копаться в тонне копипасты в коде, где используется new NumericOperation.
Применительно к ООП мне эта концепция кажется странноватой — разве там не основной принцип, что состояние должно быть вообще абстрагированно и мы не можем получить к нему прямой доступ?
Пусть читает данные кто хочет. Лишь бы не писали.
Однако немного модифицируем пример, чтобы обеспечить более строгую изоляцию:
class NumberState {
int get a;
int get b = 10;
setA(int a) {
this.a = a;
}
}
В итоге мы получили объект, который мы можем модифицировать, только если явно вызовем ту или иную функцию. Если и такой вариант не устраивает, то наворачиваем еще абстракции с иммутабельными стейтами, Stream, который доносит актуальный стейт в конструктор через подписку и т.д., и т.п.
Важное правило — читает любой, пишет только через внешние методы. О том, как лучше уже реализовывать эти внешние методы, это уже зависит от каждой конкретной команды. Кто-то на конвенциях может договориться. А кому-то нужна большая абстракция, чтобы максимально вывернуть руки.
Применительно к ООП мне эта концепция кажется странноватой — разве там не основной принцип, что состояние должно быть вообще абстрагированно и мы не можем получить к нему прямой доступ?
Это извращенная трактовка принципа инкапсуляции, отождествляющая её с сокрытием. Инкапсуляция — это не сокрытие данных от внешнего мира, а объединение логически связанных данных и функций в единое целое.
У таких статей есть непосредственная польза. Они могут помочь честнее взглянуть на ФП. Потому что многие функциональные языки описывают себя как очень хорошие и даже популярные, а на самом деле писать на них в итоге почему-то неудобно (и никто не пишет). Вот, к примеру, очень хорошая и глубокая критика языка и «маркетинговой кампании» Haskell. В этом же блоге есть свежая предметная статья с критикой чистого ФП. Такая критика и понимание проблем могут помочь сэкономить много денег компаниям, потому что они не начнут проекты на Хаскеле, к примеру. Помогут исследователям сконцентрироваться на важных аспектах этих языков и т.п. Помогут лучше учить программирование в университатах, опять же.
Вот наглядный пример. Оказалось, что в одном немецком университете есть специальная программа (типа магистерской) на факультете информатики для людей с не-ИТ образованием, где их учат программированию и информатике. И вот знакомая биолог начала по ней учиться (как раз месяц назад). И что бы вы думали? Их начинают учить с диалекта Lisp (ну, то есть, самый базовый вводный курс—на диалекте лисп). И на мой взгляд, это совершенно лишняя трата времени преподавателей и студентов (и трата налогов).
Так что я с вашим комментарием не согласен. Попытки понять, есть ли что-то неестественное в ФП и почему, имеют большой смысл. Точнее, понять, почему ФП неествественное (ведь оно совсем непопулярно) и предупредить об этом тех, кто сомневается или в неведении.
P.S. Я не спорю с тем, что какие-то общие и частные идеи из ФП, безусловно, полезны. Стараться писать чистые фунции, не изменять лишний раз состояние. Pattern matching, опять же. Но _иногда_ нарушить правила ФП намного удобнее и лучше, чем пытаться не нарушать. Стандартный пример—циклы. Писать их через рекурсию—мука. Как только у вас будет массив (список!) NumberState, с которыми надо что-то сделать в чистом ФП—все, пиши пропало. А если еще и операций будет несколько, тоже в списке…
P.P.S. Наконец, в статье не сравнивается ФП и ООП, а скорее ФП и императивное программирование.
В качестве контраргумента могу предложить перечислить крупные opensource проекты, которые написаны преимущественно с fp подходом.
Friends and collegues come to me,
Speaking words of wisdom:
«Write in C».
As the deadline fast approaches,
And bugs are all I can see,
Somewhere, someone whispers:
«Write in C».
Write in C, write in C,
Write in C, write in C,
LISP is dead and buried,
Write in C.
( https://www.youtube.com/watch?v=wJ81MZUlrDo )
Как только у вас будет массив (список!) NumberState, с которыми надо что-то сделать в чистом ФП—все, пиши пропало. А если еще и операций будет несколько, тоже в списке…
А можно развернуть, какие возникнут проблемы? Мне, хорошо знакомому с Haskell, совсем неочевидно
Их начинают учить с диалекта Lisp (ну, то есть, самый базовый вводный курс—на диалекте лисп). И на мой взгляд, это совершенно лишняя трата времени преподавателей и студентов (и трата налогов).
Смотря какая цель обучения. Если человек успешно пройдёт такой базовый курс, то с императивными языками точно разберётся.
Как только у вас будет массив (список!) NumberState, с которыми надо что-то сделать в чистом ФП—все, пиши пропало.
Что? Отфильтровать, свернуть, отразить?
React — это не ФП
Redux — да, это ФП. — Но ему противостоит ООП в виде Mobx
Redux vs Mobx -> ФП vs ООП — можно уже делать ставки кто из них одержит победу! ;-)
Но при работе с Mobx не надо заботится о том чтобы твои объекты(состояния) были immutables
Redux vs Mobx -> ФП vs ООП — Mobx одержит победу, имхо ;-)
В React из ФП есть в виде stateless components
stateless components скорее про ооп. Классический view из MVC, где stateless components — View, statefull components — толстый контроллер, а стор и екшены — тонкая модель.
React/Redux, пользуясь только функциональными инструментами
Как минимум необходимость this.setState полностью разрушает чистоту ;) А вообще на практике при сложной модели такая связка скорее становится процедурной, чем функциональной.
3 Reasons why I stopped using React.setState
https://medium.com/@mweststrate/3-reasons-why-i-stopped-using-react-setstate-ab73fc67a42e#.r4wi5oqu0
В чистом функциональном мире наоборот, эти понятия вводятся дополнительно и локально: если хочешь использовать глобальное состояние надо явно использовать объект RealWord, возможно обернутый в IO monad, можно вводить локальные состояния через State monad, причем они могут не зависеть от состояния мира и быть множественными. За это платят явной передачей этих аргументов везде, где надо.
Вскипевший Чайник ЭТО засвистевший(поставленныйНаПлиту(сНалитойВодой(чайник)))
результат = чайник;
результат.налитьВоду();
результат.поставитьНаПлиту();
результат.дождатьсяСвиста(); // каждый метод меняет состояние
Первый вариант удобен для конструирования репрезентаций данных в разных формах без влияния логики этих репрезентаций друг на друга, второй вариант удобен для работы с персистентными хранилищами данных; оба варианта одновременно возникают в информационных системах с выделенными архитектурными слоями M и V (или в компонентах/объектах с разделёнными операциями C и Q в терминах CQSR).
Спор приверженцев ООП с приверженцами ФП о преимуществах своих подходов — это спор о преимуществе двигателя перед коробкой передач.
И он однозначно изменил состояние.
Не однозначно. Вполне может быть, что он принял новое состояние, а не изменил имеющееся.
Насколько я понимаю набор правил в ФП не выполняется, а вычисляется.
В общем случае при моделировании реального мира набор правил последовательно (или параллельно) применяется к состоянию объекта, порождая новые состояния и, возможно, производя побочные действия, типа записи в логах :)
Разница начинается в тот момент, когда наблюдатель открывает ящик в середине процесса и суёт палец проверить уровень воды. В императивной парадигме у него есть шанс нащупать полунаполненый чайник, или не нащупать его вовсе, а вместо этого тыкнуть пальцем в глаз другому наблюдателю, наполняющему чайник. Чтобы этого избежать, нужно предусмотреть запирание ящика на время изменения состояния, организовать очередь желающих открыть ящик, избежать ситуаций, когда кто-то открыл ящик A и хочет открыть ящик Б, а кто-то другой — наоборот (дедлок).
В функциональной парадигме таких проблем просто нет: новое состояние либо уже существует целиком, либо не существует вовсе, и для этого не нужно устраивать кучу церемоний. Пока чайник не вскипел и пирог не испёкся, их никто наблюдателю никогда не выдаст.
Эрго, императивщина вполне работает в случаях, когда наблюдатель строго один, и его не беспокоит возможность увидеть грязное закулисье мира. В этой парадигме все наблюдатели живут в едином времени (как в ньютоновской физике), и это порождает парадоксы конкурентного доступа. Функциональщина хороша, когда наблюдателей куча, но нужно чтобы у каждого был свой собственный параллельный мир, не конфликтующий с мирами других наблюдателей. Каждый наблюдатель живёт в своём «собственном времени» (как в специальной теории относительности), что разрешает парадоксы, но труднее для обыденного восприятия.
Скорее вопрос применения неправильного типа двигателя. Например установки на тележку для гольфа дизельного двигателя, вместо электрического. И то и другое работает, но одно из решений не оптимально.
Собственно это автор и показал на примере с рецептом. Для рецепта функциональный подход не подходит т.к. это типичная императивная задача. В то же время в статьях про ФП можно найти кучу примеров, где задача через ООП делается сложно и громоздко, а в функциональном стиле просто и удобно.
Вот поэтому и надо изучать разные парадигмы, чтобы понимать, что и где удобнее.
P.S. Состояния вызываются последовательно (контекст — Чайник тоже может передаваться между словами)
Костылями зовут кривые решения, затыкающие какую-то проблему, но не устраняющие причин и не обладающие достаточной общностью.
плохо подходят для прямого описания последовательных шагов.Почему же, для описания последовательных шагов в виде цепочки функций ФП отлично подходит. Но хуже подходит для описания нескольких взаимовлияющих на разделяемое состояние последовательностей. Впрочем, до абсурда можно что угодно довести… давайте печь пирог при помощи ООП и классов Духовка, Противень, Миска, Мука, Сода, Соль, Масло, Сахар, Яйцо, Кефир, Банан, Орех, Полотенце.
Автор тупо хитрит, когда приводит рецепт из кулинарной книги в качестве императивной программы. Рецепт — это далеко ещё не программа.
давайте печь пирог при помощи ООП и классов Духовка, Противень, Миска, Мука, Сода, Соль, Масло, Сахар, Яйцо, Кефир, Банан, Орех, Полотенце.Ниже приводили пример, когда духовок, противней, мисок и всего остального имеется в наличии по много экземпляров. Тогда такой прикол с классами, состояниями и событиями будет вполне уместен.
тесто |> месить |> поставитьВДуховку |> влючить
оператор |> берет левую часть и применяет к ней функцию, указанную в правой
Вот, например, выпекание пирога на Elixir:
http://pastie.org/10879491
Запускается 4 актора (печь, противень и 2 миски) и всё прекрасно записывается при помощи отправки им сообщений.
Да, эта задача вертится вокруг состояний, но не надо думать, что функциональные языки не могут работать с состояниями. Просто они не используют для этого классы.
Признана она в лучшем случае в своем слабом варианте.
PS а вообще, математики делятся на Алгебраистов и Геометров, которые привыкли размышлять по-разному.
Почему не получилось? — Был такой теолог — Райму́нд Лу́ллий — он одной логикой(!) обращал мусульман и иудеев в католицизм в 12 веке (атеистов тогда не было, были ещё местами язычники). — Так он интересную механическую машину выводов то изобрёл! ;-)
Для начала она никогда не была. Научный метод — совокупность инструментов изучения. Можно было бы предположить, что «объективная реальность» — это «предмет» изучения науки. Только с соответствии с «научным методом» предметом изучения науки является «реальность наблюдаемая». А это, как говорят, две большие разницы.
С практической точки зрения удобно пользоваться категорией «различимое различие» (difference that make a difference). Когда субъект видит разницу между А и B, которую может как-то описать на своём языке, считается, что разница есть. Если же субъект разницы не видит или не может её описать, то значит и разницы никакой нет. Например, две картофелины примерно одинакового размера имеют разную форму. Это очевидно. Тем не менее, тому, кто будет есть картофельное пюре, это различие безразлично. Для него различия, как бы, и нет.
Философы могут гордиться результатами всего научно-технического прогресса, т.к. именно в философии сформулирован научный метод и его предмет исследования, которые привели к рассвету всех ныне существующих наук. Если уж на то пошло. Но вы, кажется, уже забыли о сути беседы (мы об объективной реальности, насколько помню, беседовали).
Существование того, о чём никто не знает, находится под вопросом. Нельзя сказать ни да, ни нет. Многие вещи существуют как чистый вымысел. Некоторые существуют как мифы. Кто-то верит в их существование, а кто-то не верит. Но обязательно нужен кто-то, кто хоть что-то об этом знает. Чайник Рассела существует, как минимум, в качестве мема. Невидимый розовый единорог тоже в некотором смысле существует. Хотя бы как пример невозможного. Вообще абстрактные категории могут существовать без всяких ограничений. Параллельное существование реальных прототипов необязательно. Есть один очень мощный инструмент абстрагирования — отрицание. Например, существуют сигареты. Все понимают, что это такое. А теперь попробуйте себе представить реальный прототип абстракции под названием «не сигарета». Не какую-то конкретную вещь, которая бы не была сигаретой, а именно «не сигарету» вообще. Это что такое? Как это выглядит? Оно видимо или невидимо? Ответов на эти вопросы нет. Потому что это чистая абстракция. Так же и невидимый розовый единорог. Это абстракция. Пример невозможного в реальном мире. Именно в этом качестве оно и существует. Только в сознании людей.
На Haskell то, что он написал, выглядело бы примерно так:
parseVarant = parseA <|> parseB <|> parseC <|> parseD
Да, я знаю, что тут не нужно путать язык и компилятор, ведь эффективность кода зависит от компилятора. Однако я пока не видел компиляторов функциональных языков, которые давали бы не тормознутый код.
Как раз в этой-то области и цветёт функциональное программирование как парадигма. Ибо чтобы распараллелить задачу надо произвости декомпозицию до простых частей, а это и есть суть функционального подхода.
Да и чисто императивных языков сейчас всё меньше и меньше (а может быть и вообще не осталось), сплошь и рядом пытаются расширять их для написания в функциональной парадигме.
на Паскале (и Delphi) в связке с ассемблером…
Так на любом в связке с ассемблером можно! lol
C#, к сожалению, нельзя
Читал рекомендации по настройке C# с отключением кучи всего и получением результата по скорости близкого к C++ Net.
На функциональном языке нельзя написать ни одной эффективной (и одновременно полезной) программы, которая выжимала бы из процессора максимум. Это можно легко сделать на Си
Сам не проверял, но читал про следующий трюк:
— берём функциональный код f(a,b)
— а затем преобразуем получая вот такое a b f
=> получился Forth! Который, по скорости вполне сравним с C => PROFIT!
на Паскале (и Delphi) в связке с ассемблером…
Так на любом в связке с ассемблером можно! lol
-1
Если есть что возразить — возражайте!
И тот и другой языки — не быстрые (сам на них долгие годы писал, в случаях когда скорость исполнения не требовалась), а возможность влепить вставку на Ассемблере — не делает сам язык быстрым.
У меня вполне получалось писать т.н. «научные расчёты» на Scala, и если иммутабельностью не упарываться (т.е. тыкать всюду массивчики и мутабельные хэш-таблицы) и память очень часто не выделять, работает всё сносно. OpenCL вполне подключается, и кластеры наверно тоже можно задействовать (мне не приходилось).
Delphi, насколько я помню, выдавал не слишком уж оптимальный код (до внедрения llvm точно) по сравнению с GCC, и язык/экосистема имхо куда более непроработаны, чем в прочих популярных языках. Если что-то распараллелить нужно — одним .par или #pragma omp parallel for не обойдёшься. В том же С++ с этим проще. По моему опыту, «эффективный код» и «Delphi» — несовместимы, даже странно слышать эти понятия вместе.
По моему опыту, «эффективный код» и «Delphi» — несовместимы, даже странно слышать эти понятия вместе.
Смотря, что именно считать эффективностью. По скорости — он, да, не быстрый, и для этого лучше C и C++.
Но, он крайне эффективен для Rapid Aplication Development — когда нужно быстро получить работоспособный ясно читаемый код работающий без ошибок.
Не буду объяснять то, зачем решать такие задачи, но если кратко, то их решение открывает в науке новые методы, с помощью которых решаются аналогичные задачи, скажем, из статистической физики (например, расчёт термодинамических свойств макромолекул, этим я занимался в своей диссертации).
Я думаю, что нет смысла доказывать мою точку зрения, просто выскажу общую мысль всех тех учёных, которые трудились над подобными задачами: функциональщина там не пройдёт ни под каким видом. А нравится это кому-то или нет — нам без разницы: ) Не пройдёт и всё. Никто пока не смог доказать обратного на реальных практических примерах.
Когда в продакшен придёт поколение разработчиков у которых эту мышцу в мозгу наоборот развивали можно будет поговорить, насколько «чистое» функциональное программирование подходит для решения реальных задач и появятся какие-то наработки по оптимальным способам декомпозиции задачи/описании архитектуры ПО. А пока, очевидно, об этом говорить рано.
У нас даже курс по ФП был (на Окамле), на котором мы поупражнялись во впихивании императивно очевидных задач в ФП, что вызвало немало боли и отвращение к предмету.
На самом деле, больше всего разобраться со всем этим мешало именно непонимание базовой структуры данных. В императивном языке это массив, тривиально отображающийся к плоской памяти Фон-Неймана. (Или, например, на лист бумаги с ручкой.)
Хочешь — пиши в него, хочешь — исполняй его пошагово.
А в ФП какая базовая структура данных? Связный список? Тот самый, в котором доступ по индексу за O(n) осуществляется?
Просто нужно перестать совать всем в лицо это ваше функциональное программирование, а просто тихо и спокойно использовать его в тех немногочисленных специфических задачах, на которых у этого подхода есть очевидные преимущества. И тогда всё у него будет хорошо. Оно будет фантастически популярно… в узких кругах.
— Event освободилась ёмкость для замешивания теста => месим тесто
— Event освободилась форма пирога => заливаем в него тесто итп
— Event освободилась духовка => ставим туда форму с пирогом
— Event пирог испёкся => освобождаем духовку и форму для пирога
ну и так далее в том же духе.
Причём всё логично и легко понятно, а на События можно реагировать процедурно.
В приведённом списке обработчиков событий каждый обработчик будет выбирать из общего пула пирог, который находится в определённом состоянии. Например, в случае обработчика освободившейся формы для пирога, нужно будет выбрать из пула пирог, который уже существует в виде замешанного теста и готов к выкладке в форму. Т.е. состояние пирога будет отделено от шагов алгоритма.
В одном из последних номеров Overload как раз была статья о том, как можно постепенно приучить нормальных программистов к функциональной парадигме через event-driven и конечные автоматы (http://accu.org/index.php/journals/2199).
PS Думаю, что если Вы переведёте, то получите плюсы в карму. :)
Неа, можно плюсовать до +4
Да и проверить просто — у людей без публикации не только красная стрелочка теперь.
bool success = false;
ParseResult<V> result;
using expand_variadic_pack = int[]; // фокус!
(void)expand_variadic_pack{0, ((success = success || (
parsers.parse(state).get_success()
&& ((result = ParseSuccess<V>{
{std::move(parsers.parse(state).get_success()->value)},
parsers.parse(state).get_success()->new_state
}), void(), 1)
)), 0)... };
и обойтись без рекурсии. Страшновато, конечно, но если parser.parse
принимает const State&
, то компилятор, возможно, даже соптимизирует три одинаковых вызова parsers.parse(state).get_success()
в приведённом коде.(Объяснение происходящего на SO)
Язык написания абсолютно не важен. Объектно-ориентированная модель может быть построена и на функциональном языке. В итоге всеравно все к этому сводится (машинный код).
Где проще использовать функциональный язык зависит только от сферы применения и расширяемости конкретной задачи.
Если надо написать статичное решение (Например: драйвер железа) которое не будет архитектурно расширятся внутрь, а только обростать новыми модулями, библиотеками или програмами. То его быстро и удобно написать на функциональном языке.
Но если есть задача построить виртуальную модель реального мира в базе-данных с множественными связями, то писать это на функциональном языке будет сложно по одной причине — исходная архитектура «трущебы». Их надо разобрать и собрать из их же деталей небоскреб. Получится дёшево но не эстетично, с оргомным превышением временного бюджета.
Для таких задач и образовалась специальность архитектор. И только он решает, что должно быть написано в функционально стиле, а что перенести в ООП. А специалист просто должен специализироваться на своем участке.
Функциональное програмирование странное по одной-единственной причине: оно… странное. Потому что в школах учат бейсику/паскалю, а в вузах плюсам и джаве.
Если взять, так сказать, Маугли от программирования и воспитать его исключительно на хаскеле/ML (предположим, лисп-машину кто-то построил и императивных процессоров наш лягушонок не видит), никаких проблем с ФП у него не будет, а когда ему в 25 покажут джаву, он, конечно, сначала малость прифигеет, потом скажет, что это придумали какие-то инопланетяне, потом проникнется и скажет, что это клевый способ решить некоторые проблемы, которые в хаскеле выходят не слишком красиво. И, в конце концов, придет к выводу, что лучше всего сочетать два подхода:)
Я рад, но в школах по прежнему учат паскалю, насколько мне известно.
PS язык Logo появился в 1967 году на основе классического LISP.
(defparameter *my-hash* (make-hash-table))
(setf (gethash 'one-entry *my-hash*) "one")
или вот
(defclass person ()
((name :accessor person-name
:initform 'bill
:initarg :name)
(age :accessor person-age
:initform 10
:initarg :age)))
(setq p1 (make-instance 'person :age 100))
(setf (person-age p1) 101)
Да, в нем можно писать в функциональном стиле. Ровно так же можно писать и в каком-нибудь C#, но тот функциональным не становится.
Собственно даже из университетского курса ФЛП помню, что никто не заморачивался в лиспе с функциональщиной, рекурсиями, свертками и прочим. Все тупо бахали setf и циклы, а преподу было наплевать, ведь сказали же, что лисп функциональный, значит все нормально.
Во-вторых, вы, вероятно, полагаете, что всё, что вы перечислили, необходимое к обучению и в императивном языке, оно значительно проще, чем State, и потому добавление State — та соломинка, что переломит хребет верблюду?
Не так давно я как раз подбирал подходящую аналогию, чтобы объяснить отличие императивного подхода от функционального. И нашёл её в школьном курсе математики.
Все мы с вами знаем, кто такие синус и косинус. И нам не приходит даже в голову пытаться их применять через определение. Мы просто знаем и пользуемся. Но в школе вхождение в эту тему имело очень ненулевой порог для многих.
Так и в функциональном программировании: однажды въехав в тему (например, монада State), мы начинаем её просто использовать везде, где она нужна.
С применением императивного же подхода нам гораздо легче начать, оперируя базовыми конструкциями, но при этом мы каждый раз воспроизводим эти конструкции от той самой базы.
Итак, начнём.
def разогретьДуховку: (Духовка, T) => Future(РазогретаяДуховка)
def подготовитьПротивень: (Противень, Мука) => Future(ПодготовленныйПротивень)
def подготовитьОсновуДляТеста: (Мука, Сода, Соль) => Future(ОсноваДляТеста)
def приготовитьЗаправку: (Масло, Сахар, КоричневыйСахар, Яйца, Бананы) => Future(Заправка)
def приготовитьТесто: (ОсноваДляТеста, Заправка, Кефир, Орехи) => Future(Тесто)
def выпечь: (ПодготовленныйПротивень, РазогретаяДуховка, Тесто, Время) => Future(ГорячийПирог)
def остудить: (ГорячийПирог, Полотенце, T) => Future(ГотовыйПирог)
let холоднаяДуховка = Духовка()
let чистыйПротивень = Противень()
let масло = Масло(450г)
let сода = Сода(1ч.л.)
let cоль = Соль(1ч.л.)
... etc
let пирог = for {
духовка <- разогретьДуховку(холоднаяДуховка,175)
противень <- подготовитьПротивень(чистыйПротивень, Мука(50г))
основа <- подготовитьОсновуДляТеста(...)
заправка <- приготовитьЗаправку(...)
тесто <- приготовитьТесто(основа, заправка, Кефир(200г), Орехи(50г))
горячийПирог <- выпечь(противень, духовка, тесто, 30мин)
пирог <- остудить(горячийПирог, Полотенце(), 25)
} yield пирог
Сложно?
Так же свою часть сложности внесет описание структур данных, они станут или очень сложными или многочисленными, например реальная духовка может быть крайне сложна и иметь огромное количество параметров. Можно конечно ввести структуру данных «духовка для пирога» и функцию «разогретьДуховкуДляПирога», но это было бы не слишком удобным решением.
В данном случае задачу можно разделить на вычисления и эффекты. Для реализации необходимых эффектов мы просто используем соответствующую монаду. Или их комбинацию.
Что мы получаем:
- ключевая бизнес-логика процесса явно читается из кода
- вычисления соответствуют принципу Single Responsibility, легко тестируются и распараллеливаются
- каждый из эффектов реализован и оттестирован в одном месте — соответствующей монаде.
Если принять, что мы при этом использовали подход Domain Driven Design, то часть сложных эффектов явно выходит за рамки нашего Bounded Context и у нас сводится к публикации соответствующего Business Event.
Как-то так. Кстати, если мы про реальный мир, то там не обойтись без обработки ошибок. Добавим:
пирог
.map(УпакованныйПирог(пирог))
.recover{
case Подгорел() => ....
case Непропёкся() => ...
case ОтключилиЭлектричество() =>...
case НахамилНачальник() => ...
default => ...
}
Ну, вы понимаете. Опять же, логика читается из кода. Я за это очень ценю такой подход.
Автор оригинала допустил фатальную ошибку. Рецепт должен быть не композицией функций, вложенной структурой данных, например, списком словарей или пар (сущность — атрибуты). И отдельный код, возможно, с побочными эффектами, для извлечения результата их этой структуры.
Как справедливо заметили выше, императивный подход хорош только для одного пирога. Когда понадобится 100 пирогов при наличии 10 человек, 5 печек и 20 миксеров — начнутся проблемы с распределением ресурсов. Как функциональный подход справляется с этим, я писал в блоге.
Типа:
(достать (печь (поставить пирог в (разогреть духовку до 175 градусов)) в течение 30 минут))
или
(достать (выпеченный (поставленный пирог в (разогретую духовку до 175 градусов)) в течение 30 минут))
Наиболее используемая скобочная нотация, возможно, не лучший DSL для описания задач, но, тем не менее, мне кажется, то, что описано в статье, не совсем правильно применять к реалиям функциональных языков. Я боюсь, что видение «головоломок» в функциональных языках происходит обычно от того, что люди пытаются применить к нему подходы, к которым они просто привыкли в процедурных или ОО языках. Но это как разговаривать на русском языке, заменяя слова английскими — речь от этого не станет английской. Это, скорее, просто разные способы мышления и подходы в постановке и обработке задач. Сложнее или проще для восприятия (лучше/хуже) — зависит во многом от опыта работы с конкретными языками и методик решения задач.
PS Как еще один вариант в абстрактной (не в LISP) нотации:
достать из печи тесто после того как оно
. простояло в течение 30 минут в
… разогретой до 175 градусов духовке
Ну и еще, как мне кажется, ФП не популярно не потому что оно странное (странных технологий немало), а потому что сильно отличается от того, подо что инфраструктура затачивалась десятилетиями. При наличии всесторонней поддержки — ОС, библиотеки, умные компиляторы, может быть даже железо — ФП может радикально повысить качество прикладного ПО.
Оно, конечно, да. Однако, суть ФП не в программировании без эффектов, а в отделении эффектов от чистых вычислений. Они выполняют принципиально разные задачи, поэтому по отдельности с ними работать гораздо проще. Реюзать, тестировать, делать код ревью...
1. Было у меня повторение кода. Плохо, плохо, нужно отрефакторить
2. Что-бы применить? М-м-м, мы же на плюсах пишем, шаблоны же
3. Какая боль, какая боль, шаблоны функциональные
4. Получилось костыльно
…
?.. Это все функциональщина проклятая
?+1. Я не против функциональных языков, но пускай там все будет как в императивных
При этом проблему можно было выразить куда проще. Шаблоны в C++ не достаточно удобны и не позволяют применить более мощные средства. Многие из которых, как ни странно, доступны в других функциональных языках.
Афтар сделал мой день
> Функциональное программирование непопулярно, потому что оно странное
оно не странное. это чуваки которые пытаются чтото функциональное изобразить на не функциональных языках — странные.
хочется фцнкциональщины — пишите на хаскеле он вполне продашкен реди сколько лет, и не трахайте людям мозг своими ссаными шаблонами.
```
pan = pan
|> grease
|> flour
oven = oven
|> preheat(«175C»)
dough = mix(«flour», «baking soda», «salt»)
cake_mixture = mix(«butter», «white», «sugar»,«brown sugar»)
|> beat_until_fluffy
|> beat_mixed_with(«eggs»)
|> mixed_with(«bananas»)
|> mixed_with(:alternating, «buttermilk», dough)
|> mixed_with(:chopped, «walnuts»)
cake = cake_mixture
|> place_on(pan)
|> put_in(oven)
|> wait_for(«30min»)
|> eject
|> put_on(:towel)
|> wait_until_cool_down
```
Как видим, вся последовательность операций линейна и понятна. Может быть это «недостаточно функционально»? Пофиг, главное что удобно на практике.
P.S. |> это pipe operator, работает так:
a |> b |> c ==> c(b(a()))
Но есть ньюанс, никто не знает как должна выглядеть архитектура функционального фычислителя ввиду его полной абстракции ;)
Зато существует, уже полвека, неймановская архетиктура и все императивные языки являются надстройкой над этой архитектурой и максимально ее утилизируют. Все Функциональные языки являются надстройкой над виртуальной машиной, которая пишется в императивном стиле и использует реальную неймановскую архетикоуру.
Так что функциональные языки это попытка построить еще один уровень абстракции над императивным языком, а императивный язык это уровень абстракции над ассемблером и тд и тп
Вопрос риторический сколько уровней абстракции нужно для решения конкретной задачи. Как то так
Так зачем, если не видно разницы, думать больше!? Сразу пишем на императивном языке ;)
Как правильно заметил автор, 95 процентов задач вполне решаются императивным подходом, плюс нароботана десяти-двадцатилетняя экспертиза по решению, плюс библиотеки
В реализации сила и слабость функционального языка, ибо с одной стороны язык не запрещает никаких способов реализации которые бы давали правильный ответ, но и никаким образом не подсказывает, какая реализация могла бы быть опимальной. Вся реализация лямбда исчисления и фп — математические концепции в голове разработчика.
никто не знает как должна выглядеть архитектура функционального фычислителя ввиду его полной абстракции ;)
А что не так с вариантом?
— берём функциональный код f(a,b)
— а затем преобразуем получая вот такое a b f
=> получился Forth! Который, по скорости вполне сравним с C => PROFIT!
Никто по поводу него критически так и не высказался.
То есть концептуально, для разработки сложных систем, ничего не поменялось, нет никакой страховки, что супер-пупер навороченная функция из десяти тысяч строк не содержит ошибки и не вынесет весь прекрасно выглядещий абстрактный и правильный вобщем код на крах системы
А вообще, сколько раз (за двадцать лет программирования) я уже слышал о «новых прорывных» технологиях, которые сделают хорошо программисту, а воз и ныне там… уже не верю ни в серебряные пули, ни в эльфов из Микрософт, что выкуют новую ОС и язык программирования всеобщего счастья, как-то так.
Да и функциональное программирование живет себе уже 40 лет и уже десятое поколение начинающих программистов-фанатиков обещает всеобщее счастье и прогресс, реальность топчет все эти обещания кованным сапогом… в итоге имеем промышленный стандарт с++\c# и Java в которых что-то можно описывать в функциональной парадигме посреди императивного кода
И мне кажется непопулярность ФП это в некотором роде выдумка. Если писать огромную систему документооборота (например), то в этом случае без ООП не обойтись. А если писать например утилиту для системного администрирования, которая уместиться в один файл, то ООП это overkill.
ООП и ФП это просто инструменты которые нужно правильно применять. И право на существование имеют оба подхода.
Значит, вначале был императивный подход с такими особенностями:
— однопоточность,
— отсутствие хранимых состояний,
— чистые функции.
Потом появилась необходимость сохранять состояния (чтобы вызывать их и обрабатывать в любой момент) — появилось ООП со следующими особенностями:
— частичная многопоточность,
— сохраняемые состояния,
— отсутствие доступа к чистым функциям (которые оказались инкасулированны внутри объектов, «загрязненных» состояниями).
Потом стало нехватать многопоточности и вспомнили про ФП, где есть:
— доступ к чистым функциям (конечно, функции сами по себе всегда чисты, но так уж повелось),
— неограниченная многопоточность,
— отсутствие состояний.
Можно конечно «в лоб» объединить ООП и ФП в одном языке, но почему бы не поступить так:
— есть чистые функции (как в ФП),
— есть недо-объекты (в которых хранятся только данные), назовем их Н-объекты (они являются только носителями состояний),
— Н-объекты вызывают чистые функции, которые передают выходные данные в: вызывающий Н-объект; другой Н-объект; другую функцию.
Получаем два множества сущностей, в одном — Н-объекты, в другом — чистые функции. Можно переходить к графическому программированию, где отношения между ними будут задаваться в виде схемы (что будет очень наглядно, быстро и надежно).
ООП и ФП вообще разные вещи и никак друг-другу не противоречат.
Значит, вначале был императивный подход с такими особенностями:
…
— отсутствие хранимых состояний,
...
Когда такое было? На ткацких станках с перфократами?
Функциональное программирование непопулярно, потому что оно странное