Pull to refresh

Comments 90

Перевод хороший, но наверно не стоило… очень много воды и пританутых «за уши» высказываний. Моей маме (домохозяйке) наверняка будет интересно прочитать про акулу и это единственное, что, пожалуй, стоит ей знать, читая этот пространный опус «почему функциональные языки программирования ништяк». С коллегами так только после обкурки их чем-то забористым получится.
Ну так для опытного программиста это так, почитать забавно так как это все же журнальная, а не научная статья, ну и лишний раз убедиться что «я все делаю правильно, я молодец». А для начинающих полезная.
Довольно часто обзорные статьи помогают лучше понять проблемы чем узкоспециализированная. Аналогии из других областей помогают мышлению. Ведь как говорил Козьма Прутков: «Специалист подобен флюсу, полнота его односторонняя.»
Я, конечно, не эксперт, но бороться с глобальным состоянием в программах, где оно логически должно быть — это как-то странно. Вы же все равно в итоге будете его как-то эмулировать, если я не ошибаюсь.

Например, как можно написать базу данных без глобального состояния? Хранить глобальное состояние в другом месте и оттуда его каждый раз читать?

Или из более простых вариантов, систему алертов для мониторинга, на подобии alertmanager, где глобальное состояние, это единственное, что в этой программе вообще есть.
Вы упустили суть:
Глобальное изменяемое состояние.

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

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

Но вообще говоря, если вы испытываете нужду в изменении глобального состояния, при этом используя функциональный стиль, то вы уже делаете все в корне неверно.
Если у кого-то возникает потребность в «логической необходимости» учёта глобального состояния программы, значит он сталкивается с кардинальным просчётом в архитектуре проектируемой системы.
Ну вот, я привел вам пример архитектуры приложения. Оно просто получает сообщения про алерт, хранит данные про этот alert, что бы потом сделать expire, если алерт не приходил заданное время.

Как на меня, тут на лицо глобальное состояние в программе. Какой просчет при проектировании был допущен?
Есть практики, типа в Go “Do not communicate by sharing memory; instead, share memory by communicating.”

Данными, горутины(Golang)/процессы(Erlang, elixir) обмениваются через каналы/ящики, своеобразные входящие очереди, которые имеют своё собственное изолированное состояние.

Все решается не управлением глобальным состоянием с алертами, а соблюдением single responisibility между ответственными за модификацию данных.

Также есть практики типа redux’a в Js, основанных на реплейсом глобального стейта через чистые функции.
Ну, то есть в первом случае вы предлагаете мне раздробить глобальное состояние на много мелких локальных, которые будут все равно между собой в нужные моменты взаимодействовать.

А во втором случае опять заниматься синхронизацией изменений с разных потоков. И все это с довольно туманными целями. Потому что не понятно, что мне помешает поломать состояние при пересоздании так же, как и при модификации.
Эти концепции позволяют в любое время масштабироваться. Для мелких целей есть мутекс.
Глобальное состояние можно так же масштабировать через внешний сервер, например, кеш или базу данных.

Я просто не совсем понимаю, чем же так плохо глобальное состояние в программе, кроме того, что его можно как-то повредить. Потому что вопрос о повреждение глобального состояния это больше вопрос к написанию кода и тут можно наделать чего угодно и на ФП.
Все ваши сомнения и сарказм мигом улетучатся, если вы овладеете ООАДП.
Из вашего последнего абзаца (
А во втором случае опять заниматься синхронизацией изменений с разных потоков. И все это с довольно туманными целями. Потому что не понятно, что мне помешает поломать состояние при пересоздании так же, как и при модификации.
) я понял, что вы про активные объекты не слышали. Причём, я прошу заметить, что при проектировании на уровне системы активных объектов у разработчиков, при разговоре на уровне их предметной области, появление слов «поток» и «мьютекс» — показатель лютой непрофпригодности. :)

Вот эти все ваши "при разговоре на уровне их предметной области" и прочее показывает, что вы страдаете самой большой проблемой любителей ФП, из-за которой очень много людей не воспринимают ФП серьезно. Вы теоретизируете.


Теория это круто, но значительная проблема в том, что за поддержку ФП парадигмы нужно платить, и платить удобством, логикой работы и прочим. Все выглядит офигенно в теории, а на практике накапливается куча низкоуровневых проблем, обходов и прочих костылей для того, что бы таки сделать или сэмулировать глобальное состояние в тех местах, где без него надо переписывать половину проекта. И вот так ваше "чистое" ФП превращается в не совсем чистое, "чистые" функции начинают иметь сайд-эффекты и вся прекрасная ФП теория разваливается на части.


Сейчас у кучи современных программ, сайтов и прочего если глобальное состояние — это база данных, кеш, пул коннектов и прочее. Они тоже совершили ошибки проектирования?

Вы, скорее всего, попутали адресата ваших упрёков на счёт ФП.
Связь между мной и ФП ограничивается только наличием набора книг Душкина и прочих корифеев ФП на одной из моих книжных полок, но прочитанных максимум до благодарностей в разделах введений в этих книжек. :)

А вот с ООП я работаю с 1988 года.
… и, как мне кажется, вы совершенно неверно воспринимаете или интерпретируете понятие «глобальное состояние». В последнем абзаце вы «агрегировали» состояния отдельных объектов, реализующих функциональности БД, кешев, пулов коннектов и «прочего» в некое «глобальное состояние».
Поймите, что, упоминаемые вами, «кеши» и «пулы» — лишь инструменты/структуры для реализации объектов более высокого уровня.
Обычно на уровне предметной области (от встроенных систем и — до, например, управления каскадом ГЭС или программ моделирования экономических систем размером с государство), понятий «кеш» или «пул» — НЕТ. Если же они там появляются, значит авторы модели предметной области смешали семантические уровни в своих моделях.
Даже если у меня есть «пулы» на уровне, например, какой-то ОСРВ или библиотеки, то напрямую нагружать их понятийностью или реализацией функционала из предметной области — будет ОГРОМНОЙ ошибкой.
Списки, кеши, пулы могут реализовывать и поддерживать внутренние и состояния выделенных объектов предметных областей, но сами они НЕ МОГУТ являться носителями сущностей этих областей.
Это примерно также, как с широко распространённой ошибкой получения «активных объектов» через наследование от класса Поток во многих библиотеках классов (от Delphi и — до Явы). Авторы этих библиотек сделали колоссальную методологическую ошибку, позволив неокрепшим молодым умам программистов реализовывать активность объектов в своих системах через реализацию некоего унаследованного от класса Thread виртуального метода Execute или Run. В результате массовое программистское мышление не только было отодвинуто от верного шаблона реализации активных объектов, но и убеждено в том, что именно так и надо делать…
В предметной области обычно НЕТ понятия «приложение». (Если только вы не работаете на уровне ядра ОС или её планировщика процессов)
Что за объект предметной в вашей области у вас ответственен за реализацию понятия «место хранения данных из приходящих сообщений»?
Толку с того, что Clojure — ништяк? Рынок труда требует иных героев…
мы сами делаем выбор на чем писать.

выбирать в 2018 PHP как минимум странно.

А что плохого в php 7.x ?


строгая типизация уже есть, замыкания не хуже чем в js, чистые функции — да пожалуйста, единственно параллельного выполнения нет, но для языка который создан работать в один поток и умереть это не особо и нужно.

Есть =) Только о «тиках» и мультистримах мало кто знает. В любом случае на PHP можно, например, один раз запустить и два раза «умереть». Ну или внедрять определённый кусок кода (например профилер) раз в N тиков. На практике такие штуки (не тики, а стримы) используются, например в guzzle во время параллелизации асинхронных запросов (которые промизы возвращают).

Ну и да, PHP многопоточный. Сборка TS об этом как бы явно говорит ;) Просто чаще используют NTS, с ними проще, да и это дефолтная поставка в виде fpm.

Вы меня уделали. Совсем забыл об этих особенностях хотя guzzle довольно часто использую. Но сути это особо не меняет. Почему-то все относятся к php как будто он застрял на версии 5.3 и да, тогда он действительно был не самым лучшим языком. Но с php 5.4 и выше это уже вполне серьезный язык со всем что должно быть в серьезном скриптовом языке для web. Даже несмотря на то что js в данный момент дико популярен мне в нем действительно не хватает многих вещей которые есть в php из коробки.

У PHP, имхо, никогда не было такой границы, где он был «серьёзным» или «игрушечным». Самый значимый прогресс самого языка, когда сама стилистика кода менялась, был в периоды php 4 -> php 5.3. Даже вот это упоминание «7.х» — это лишь в основном маркетинговые словечки для далёких от PHP. Всё что добавилось — это тайп-хинтинг для некоторых скаляров и анонимные классы, в остальном он ничем не отличается от того же, например, 5.6. В любом случае, стилистика кода никак не изменилась.

Разве нет?

Как раз такие мелочи вроде операторов ?? Или <=> плюс типы в том числе и возвращаемых значений когда их накопиться достаточно много и создают эффект изменения стилистики.


Язык все ещё развиваться пусть и не такими ударными темпами как раньше.

Да, null-coalesce (??) — это огонь. Без него уже как без рук, а на isset смотреть не могу уже =)


Но в целом я говорил о значимых изменениях как о "перевороте". Всё же эпоха php 4 и ранней 5ки открыли нам такие проекты, как вордпресс и битрикс, от упоминаний которых до сих пор содрогаются стены в офисах в разгар рабочих дней. И если во втором случае просто разрабы оной штуки просто… Кхм… Хз как культурно выразиться, не важно. То вордпресс изначально проектировался по принятым в те времена подходам. С другой стороны же можно сравнить какую-нибудь Symfony 2.x и Symfony 3.4+, требования PHP совершенно разные, но стилистика и идеи совершенно идентичные, потому что изначально писался адекватными людьми. Ну или Laravel 5.x, но там чуть сложнее, есть и адекватные сырцы, есть и дичь а-ля Yii (извини, SamDark )))).

На всех найдётся работа. Я год назад перешёл на react native и забрал ту единственную вакансию что была.

Цезура ради качества, свобода — это рабство, война — это мир.

Автор считает, что все ошибки в программах — фатальные. Это совсем не так. Учитывая, что 90% кода пишется не в функцинальной парадигме и ПО постоянно становится лучше, то явно нет никакого безусловного преимущества функциональных языков. Да, иногда они лучше, для них есть ниша. Но это не универсальное решение всех проблем.

Всё зависит от целей, если соседняя АЭС бахнет, потому что её ПО станет лучше только в будущем легче от этого вам не станет.
Для игрового компа можно на чём угодно писать софт, но есть масса применений где важнее качество.
UFO just landed and posted this here

Скорее на MISRA C, а оно имеет мало общего обычным C.

Я в свое время участвовал в разработке ящика, который потом уехал на АЭС. Писали мы на С. Признаюсь честно. Мы очень старались не набокопорить ;)
Я примерно о таком думал — хакеры любят «свободу», им по душе си, перл и подобные языки где можно сделать что угодно и как угодно, они утверждают что возможность выстрелить себе в ногу это не страшно, просто надо больше ботанить документацию и не стрелять в себя.
Но это пустая гордыня — ошибаются все, даже самые крутые хакеры, да и их очень мало, никак не хватит на то чтобы писать весь софт.
Обычные разработчики ночью спят а не зубрят документацию на язык, поэтому считаю, что подход описанный в статье абсолютно правильный, впрочем я уже писал об этом.
Вопрос в том, почему хакеры, выбирая опасный инструмент, получают лучшие результаты, чем «обычные разработчики» с безопасным.
Не вижу вопроса — потому что они хакеры, потому что это их прёт и они этим занимаются с полной отдачей, они самомотивированы. Это круто, но их слишком мало, чтобы на этом строить индустрию.
Вопрос, почему не прёт от безопасных инструментов, ведь с ними якобы можно сделать быстрее, больше, надёжнее.
У меня нет ответа на этот вопрос. Но несколько предположений можно сделать.
Во-первых, потому что ФП не везде есть в образовании, о нём многие просто не знали, да и сейчас не знают.
Во-вторых, весь софт написан на условном си, а значит этот си и выгодно изучать, это тоже не мотивирует к изучению ФП.
В-третьих, преимущества ФП во многом в лучших возможностях по формальной верификации, а пока от общества нет заказа на верифицированных софт, это значит, что выгоды меньше. Т.е. это в том числе и политический вопрос.
В-четвёртых, хакеры часто самоучки, а всякие хаскелы в академической среде, это как параллельные миры.
В-пятых, как я уже сказал — гордыня, они думают, что только дураки делают ошибки потому что плохо доки читали, а я то хакер — клац, клац и в продакшен, буду я ещё всякие типы компилятору указывать. Мудрость приходит не сразу, а мудрость в данном случае в том, что ошибаются все, что человеческий мозг это ненадежная, нестабильная система. Немного не выспался, потеря внимательности и хоп — баг. Отвлекли вопросами — не полностью восстановил контекст и хоп — баг и т.д.

Может потому что разучивать кучу функционала, которые написаны на разных языках и технологиях, которых сотни — и выбираются они из некого пула, который они слышали, знают и тп. А в итоге, все будет запущено на яве машине или скомпилировано в машинный код, и все это будет работать на железе. У всех этих вещей есть языки, которые ближе к ним и с которыми все равно придется копаться.
Если коротко, то — удивительно, почему бы не использовать огромный набор инструментов для вскрытия двери, а используют лом и отмычку. И получается эффективно.
Чтобы противостоять целой индустрии, нужна инициатива, упорство, коммуникация и талант. И если меньшинство будет спорить фп или не фп, строгая или динамическая, то в распрях не будет достаточно эффективности, чтобы противостоять более четкой или отлаженной системе защиты информации.
Плюс есть традиции. И надежные инструменты затачивают для созидания нужд большинства.

Все просто, потому что достижение безопасности чревато:
1. Обрезанием функционала
2. Уменьшением производительности.
Итак, ограничения делают всё лучше. Гораздо лучше.

Отличный тезис. С одной стороны, разрушается абсурдным «Интересно, автор себя уже ограничил гробом?».
С другой стороны, вызывает довольно интересное следствие: «Ограничения делают все лучше, так что ограничения стоит ограничивать.» Далее следует «Ограничивать ограничения ограничений»…

Что до восхищения ФП — почему-то автор не указал ограничений его применения (вроде таких).

> (вроде таких)

посмотрел и добавил там комментарий по каждому пункту, хотя я не настоящий сварщик, но тут достаточно здравого смысла
Да я тоже не спец. Проблема в том, что большинство статей ругающих ФП заостряют внимание на производительности. На чертовой производительности, которая колышет от силы 10% разработчиков (из них 9% используют либы созданные оставшимся процентом)! Допускаю, что во время зарождения ФП быстродействие было критичным для всех, но сейчас гораздо больше усилий уходит на анализ требований заказчика, чем на решение проблем с производительностью(по личному опыту).

Получается интересная картина: недостатков (критичных) нет, ФП комьюнити потихоньку развивается\разрастается, но массовой миграции не видно. Почему?

В довесок: я кодил настолки на ФП (успешно) и на ООП (не особо успешно). Видел нечитаемые WebAPI проекты на ФП и вполне приличные WebAPI проекты на ФП. Может выбор ФП-ООП не особо сильно влияет на успех проекта\читаемость кода?
Да, более того — сравнивать производительность хаскела и си это примерно как сравнивать производительность джавы и си — разного уровня языки, очевидно, что си будет их обгонять.

Таких «почему?» полно в ИТ, видимо отрасль сложная и за малый срок существования ещё не осилила определить хорошие практики, всё развивается стихийно, дурдома хватает.

И естественно ФП это не серебряная пуля, как гласит один из законов Мёрфи — можно сделать защиту от дурака, но только от неизобретательного.
Есть такие альтернативномысящие, что какой крутой инструмент им не дай получишь фэйспалм.
Вы прослушали проповедь юного адепта секты функциональщиков.
Недостатки скромно не упомянуты, куча фактических ошибок и тп.
Например, в таких непопулярных по мнению автора языках как Си, плюсы, пхп goto присутствует. В javascript нет такого ключевого слова, но сам goto есть. В конце концов, на любом нормальном языке программирования должна быть возможность писать на фортране (это не шутка, если что).
vanxant а можно подробнее про недостатки?

Потомучто программирование это бизнесс. И начальник хочет чтобы ты был гуру "его языка" и "его библиотек". Нечего сидеть на работе и JS изучать. Там драйвер у заказчика падает на железе с Win XP. Ограниченный программист под NDA — хороший программист.

Со временем, новые языки программирования полностью отказались от поддержки GOTO.

И добавили поддержку throw/catch. Структурно то же самое, но только без усов.
Не раз и даже не два видел бизнес-логику, основанную на типах исключений. Да и сам пару раз такое писал, чего уж греха таить.
Задумавшимся об альтернативах — ссылка на полезное видео
Я совсем не гуру, но даже для меня бизнес логика на исключениях- уже сильно перебор.
Судя по всему, мне стоило не лениться и описать сценарии более детально.
Пример 1.
Используем какой-либо внешний сервис, и он бросается исключениями, например, если ему были переданы некорректные параметры. А мы, в итоге, вынуждены заниматься разбором текстов и/или типов исключений, чтобы направить логику работы по тому или иному пути. Например, банально определить, прокинуть ли текст исключения в исходном виде до UI пользователя, потому что это исключение с сообщением о бизнесовой ошибке. Или же это техническое исключение (связь отвалилась, сервис лежит) и надо замаскировать сообщение.

Пример 2.
Код бизнес логики для большого и толстого кейса. Реализовано это все в виде кучи классов/методов — как кому угодно, не суть. Главное это большой уровень вложенности. Метод вызывает метод, который вызвал метод, который вызвал метод.
И где-то в глубине нужно по определенному условию прервать вообще всю операцию. Как такой обычно пишется? У всех методов в цепочке сделать возвращаемое значение и ставить if-ы для проверки по всей цепочке вызовов? А если методы должны по бизнесу какие-то значения возвращать? Решение, которое я видел много-много раз — выбрасывать exception аля BusinessLogicException. А на верхнем уровне стоит catch на этот тип исключения, который не делает ничего, просто глушит операцию.

Возможно, у нас с вами совсем разные условия обитания, но я с такими примерами сталкиваюсь постоянно.
В приложенном видео как раз приводится резонный и простой способ избегать подобных ситуаций.
Точно! Что бы софт был быстрым и не требовательным. Разработчиков надо заставить разрабатывать свои творения на железе прошлого века. Так сразу будет ощущаться вся боль и страдания. Но в результате на современном железе будет всё летать.
На практике имеем обратную ситуацию.
Главная цель не упростить жизнь программисту или получить самое эффективное решение, а прибыль. Так что победит решение предсказуемо позволяющее получать большую прибыль.
Разработчиков надо заставить разрабатывать свои творения на железе прошлого века
Если применить к игровой индустрии, фирма получит продукт уровня Doom2 с бюждетом и сроками обычного AAA-проекта. Но зато летать будет на современном железе, это точно.
Но ведь закон Мура — он не производительность, а про количество транзисторов
UFO just landed and posted this here

В русскоязычной среде все еще властвуют предубеждения и мифы о ФП. В то время как в западной культуре разработки ФП уже несколько лет мейнстрим. Я связывают это с незрелостью и малым бэкграудом многих разработчиков.


Противники ФП повторяют раз за разом одни и те же мифы, абсолютно не понимая, что такое ФП. И часто они приходят к функциональщикам с императивным решением своей задачи вместо того, чтобы прийти с самой задачей, и ставят в вину функциональным языкам, что нельзя повторить императивное решение. Налицо XY-problem и непонимание базовых вещей дизайна ПО. То есть, такие вопросы задают незрелые кодеры, которые еще не вышли на уровень проектировщиков ПО, и которые мыслят категориями "как запрограммировать то-то и то-то", а не "какой дизайн и архитектура удовлетворят всем или почти всем требованиям?". И у стороннего наблюдателя создается ощущение, что у фпшников нет адекватных ответов. На самом деле, ответ на вопрос "как записать в БД значение, если в ФП нет побочных эффектов, а сама БД — это состояние, которого в ФП тоже нет" прост:


  • состояние есть, разные виды (чистое иммутабельное и нечистое мутабельное);
  • эффекты тоже есть, но контролируются;
  • возьми и запиши, как делаешь в обычных языках;
  • нет, от этого программа не перестанет быть написанной в функциональному стиле;
  • почему тебе нужно именно записать что-то в БД? Ведь это конкретное решение. Давай идти от задачи, а не от решения. Давай мыслить абстракциями, интерфейсами и паттернами вместо деталей реализации. Мы не решаем отдельную маленькую задачу записи в БД, мы пишем приложение целиком, где есть данные и их трансформация, и задача внести запись в БД несущественна на фоне задачи дизайна всего приложения. Может, в части общения с внешним хранилищем лучше подойдет специализированный DSL? Или STM? Или FRP? Или даже акторы/очередь/whatever? бе не подучить ФП, как ты учил ООП? Почему мы судим с позиции ООП/императивного программирования полноправную параллельную ветвь индустрии ПО? ФП тоже абстракция, тоже имеет свою область знаний, и тоже добавит в копилку новые инструменты дизайна ПО.

Когда начинаешь мыслить такими категориями, когда уже по трогал не один язык и не одну парадигму, уже не возникает мысли "а как же производительность?" или "все равно все компилируется в императивным код", потому что пересекаешь черту зрелости, переходило из разряда кодеров в разряд инженеров, которые не задают вопросы "как сделать то-то и то-то", а идут и ставят эксперимент/делают прототип/изучают вопрос. Это — зрелый подход, и требуется время, чтобы к нему прийти.


Мой доклад "Вы не понимаете ФП":
https://youtu.be/jSkYvNqQWqs

Сорри за очепятки, эти экранные клавиатуры меня убивают.

в западной культуре разработки ФП уже несколько лет мейнстрим. Я связывают это с незрелостью и малым бэкграудом многих разработчиков.

Почему вы считаете, что зрелость\бекграунд зависят от географического положения? Будучи на западе я видел как абсолютных мамонтов, так и последователей ФП. При этом называть ФП мейнстримом я бы не рискнул.

Вот корреляция между платформой и стилем есть, в Java ФП популярнее чем в .NET (по личным наблюдениям).

То есть, такие вопросы задают незрелые кодеры, которые еще не вышли на уровень проектировщиков ПО, и которые мыслят категориями «как запрограммировать то-то и то-то», а не «какой дизайн и архитектура удовлетворят всем или почти всем требованиям?».

Сильно не уверен, что подобные высказывания добавят убедительности аргументам. Ярлыки за гранью вежливости вызывают негативные эмоции, которые распространяются заодно и на ФП.

Из личных знакомств — лучшие разработчики просто фигачат код, закрывая фичи. Иногда в императивном стиле, иногда в функциональном. Агрессивные функциональщики (опять-таки, лишь личный опыт) мыслят «абстракциями, интерфейсами и паттернами», тратя две недели на задачу закрываемую в два дня.

В чем вы действительно правы — "Почему бы не подучить ФП, как ты учил ООП?". Стиль ведь действительно интересный.
Вы во многом правы, но кое в чем наши набоюдения расходятся. Говоря о западе, я имел в виду не совсем географическое положение, а, скорее, стиль мышления. Из личного опыта я вынес, что значительная часть русскоязычного программистского сообщества не готова к ФП и противится этим идеям. И говорю я, прежде всего, о плюсовиках, джавистах, php- и python- разработчиках (опять же, не всех). А вот JS-сообщество радует прогрессивностью мышления и широким использованием ФП-практик в повседневных задачах. О Java у меня другие сведения, а именно: с ФП там все плохо в сравнении с C#. Но если мы говорим о платформах .NET и JVM, то чаша весов на стороне последнего, благодаря большой популярности Scala и небольшой — Clojure.

Почему я считаю, что на Западе ФП — стал мейнстримом? Потому что этой парадигме начинают отдавать предпочтение все чаще, рассматривая ее как равную параллельную ветвь, а не выкручивая кредит доверия в сторону ООП.

Подход «фигачить фичи» я тоже видел, и видел (исправлял) последствия этого. Это плохой подход. Бизнес обманывается тем, что продукт «развивается» быстро, кодеры обманываются тем, что они круты и продуктивны. В результате продукт очень быстро превращается в тыкву, но может плыть по инерции довольно долго, а потом приходится тратить огромные ресурсы на его исправление, нанимая уже более дорогих разработчиков. А те, кто его начинал, во-первых, ничему не научились, а во-вторых, уже свалили в следующее место, рассказывая, как они подняли проект, какие они молодцы. Или, что хуже, они просто сидят на одном месте, потому что в глубине верно оценивают свой уровень как не очень высокий. Но как вы думаете, почему почти все технологии, фреймворки и библиотеки — не наши? Причин здесь много, экономическая — в первую очередь, но так же есть и причина в отсутствующей потребности у наших собратьев эти технологии изобретать, ибо они «фигачат фичи».

Заметьте, я говорю не только о ФП, а в целом. Мне совсем не весело это писать, и я, возможно, еще получу очередную порцию тухлых яиц в свой адрес, но меня уже порядком достала толерантность по отношению к непрофессионализму, выражающемуся в слепом отрицании простых истин. Можно возразить, что это не добавляет очков в пользу ФП или в мою лично пользу, — и я это прекрасно осознаю, но у меня больше нет цели кого-то переубеждать. Только поддерживать отличную идею ФП на плаву. И поскольку мои слова направлены в сторону абстрактных кодеров, то это личное дело каждого: проассоциировать себя с ними или нет, и уж тем более личное дело — оскорбляться или нет. Ну а перенос эмоций с человека на предмет, о котором человек говорит — достаточно распространенная ошибка.

А что касается сути, то будете ли вы оспаривать следующие утверждения: задающий вопросы, подобные моему — незрелый кодер; такой кодер не стал еще проектировщиком, мыслит менее абстрактными категориями; ФП — довольно абстрактно и потому кодеру доступны лишь его малые проявления (те же лямбды), но не функциональный стиль и не функциональный дизайн. Заметьте, в моих словах нет оценки «плохой/хороший», а есть оценка «еще не умеет/уже все умеет» (привет госпоже П.). Мы все такими были, но мы расширяем свою квалификацию, свой бэкграунд. Плохой кодер/разработчик — тот, что не учится.

А знаете, что, по моим наблюдениям, мешает кодерам использовать даже самые простые идеи ФП, такие как иммутабельность и чистота? Вы не поверите: неспособность или нежелание писать чистый код. Да-да, тот самый, по Мартину и МакКоннелу. Люди хотят писать лапшу, потому что они так мыслят, и у них не возникает желания упростить себе работу в будущем. Часто им это и не нужно, потому что проблемы исправляются еще большим кодингом вместо переосмысления своих подходов и практик. ФП не терпит лапши, — если мы говорим о его правильном понимании и применении. Предвосхищая ваши мысли: я видел лапшу на функциональных языках тоже. Причин было ровно две: непонимание ФП с попыткой писать «как привык»; неверные дизайнерские решения по отношению к задаче. Первое исправляется " в человеке" более плотным изучением ФП, второе исправляется «в проекте» рефакторингом или редизайном. Но ничто, никакие практики не могут исправить кодера, пытающегося писать лапшу функционально и делающего вывод, что ФП — это лажа, а простые истины, как в статье — мировой заговор ФПшников. Нет смысла браться за ФП, если ты не готов изменить мышление и пополнить свой бэкграунд. Знаете, я начал писать чистый код только потому, что видел проблемы, к которым ведет нарушение простых принципов: DRY, SOLID, low coupling/high cohesion, и я совершенно логичным образом пришел к ФП, сам того не понимая (в качестве доказательства можете глянуть мой дипломный проект, про который я когда-то давно рассказывал на Хабре). Поэтому Haskell пошел как по маслу. Я уже умел избегать лапши в коде, а на это уже наложились новые подходы из мира ФП. И после этого уже стало очевидно, что best practices исповедуют те же принципы, что и ФП, только искаженные в свете ООП. Возьмите паттерны проектирования из GoF: да большая часть из них имеет функциональную природу, и решает проблему слабой выразительности конкретных языков. С применением ФП то же самое можно сделать в разы короче, чище, понятнее и композабельнее. Но ФП — гораздо, гораздо шире. И интереснее.
А знаете, что, по моим наблюдениям, мешает кодерам использовать даже самые простые идеи ФП, такие как иммутабельность и чистота? Вы не поверите: неспособность или нежелание писать чистый код.

Вы хотите сказать, что код без иммутабельности и чистоты не «чистый»? Несколько категорично, не находите?

я видел лапшу на функциональных языках тоже. Причин было ровно две: непонимание ФП с попыткой писать «как привык»; неверные дизайнерские решения по отношению к задаче.

Поменяйте «Ф» на «ОО» — и фраза останется достаточно правдивой.

Я уже умел избегать лапши в коде, а на это уже наложились новые подходы из мира ФП.
То есть, писать без лапши можно и вне функционального стиля. Так в чем его преимущество? И чем придется платить?
> Вы хотите сказать, что код без иммутабельности и чистоты не «чистый»? Несколько категорично, не находите?

А в чем категоричность? Разве не очевидно, что в моем утверждении иммутабельность и чистота выступают примером и не являются полным охватом практик, которые приводят к чистому коду? Я могу назвать другие тоже: дробление задачи на маленькие кусочки; чистые функции там, где нужно преобразование данных; принцип единственной ответствености (SRP); принцип разделения ответственностей и выделение интерфейсов (ISP); Don't Repeat Yourself (DRY); и так далее. Можете ли себе представить, что люди не знают об этих принципах, даже проработав разработчиками много лет? И что самое печальное, они не приходят к ним естественным путем. Не нужно знать принцип SRP, чтобы сделать часть кода, ответственную только за одну задачу, ведь это упрощает использование кода, повышает его понятность и тестируемость. Однако, кодеры предпочитают отладку тестированию, что очень плохо. Отладки должно быть как можно меньше, ведь это показатель, что ты не понимаешь своего кода, какие там есть контракты и как он себя должен вести.

И я вот это вот все вам рассказываю, но я уверен, что мы с вами понимаем, что это все совершенно тривиальные вещи. Иногда приходится жертвовать чем-то в угоду конкретных преимуществ, например, — в угоду производительности. Однако делать это нужно осознанно, ведь та же производительность нужна вряд ли больше, чем в 10% задач, а из них — вряд ли больше, чем в 10% кода. И нет никаких проблем писать код изначально чистым, или почти чистым. Это не приводит к дополнительной трате времени на разработку, — после определенного количества практики. Хотя поначалу, конечно же, следует изменить стиль мышления с «фигачения фич» на осознанный кодинг. Преимущества, я думаю, тоже ясны: у вас сразу же будут части программы, которые легче понимать, тестировать и рефакторить; будет возможность развивать отдельные компоненты без затрагивания соседних, потому что вы разделили ответственности и разделили также интерфейсы с реализациями; будет проще понимать и соблюдать контракты; будет возможность разделить тесты на юнит-, функциональные и интеграционные; вы начнете мыслить систематически и в терминах дизайна, а имплементация станет лишь следствием принятых решений. Как видите, преимуществ много, и не я их выдумал. Возьмите любые статьи по best practices, — там все это будет, но по каким-то причинам некоторой части наших коллег это неинтересно.

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

Что касается ФП, то оно принуждает использовать практики. Если вы пишете императивно на ФЯ, или пишете лапше-код — вы что-то делаете не так, и это не ФП.

Код необходимо дробить на очень мелкие части, выносить функциональность в отдельные функции, собирать из них функции более высокого уровня, а из этих новых функций — функции еще более высокого уровня. ФП — это конструктор, и чем правильнее у вас базовый набор кирпичиков, тем проще вам строить всю бизнес-логику. Не только проще строить: в идеале, код, отвечающий за бизнес-логику, становится просто текстом, который вы читаете и понимаете, что ваша программа должна делать и как себя вести. А код читается значительно больше, чем пишется.

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

В ФП никак нельзя нарушать принцип SRP, потому что функция не будет композироваться с другими. А лапше-код именно такой: в нем все намешано, порой много разных факторов взаимодействуют нетривиальным образом, — и если создать такую функцию, она не будет ни с чем компоноваться. Она будет миром в себе, этаким деревянным кубиком в груде конструктора Лего, который ни к чему толком не подходит.

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

Собственно, гарантии, приходящие из чистой части кода, крайне облегчают его дальнейшее сопровождение. И, пожалуй, это наиболее полезное следствие ФП, отличающее его от ООП. Ведь ООП — императивно по своей сути. Без тех же явных контрактных аннотаций или юнит-тестов ваш код не будет давать никаких гарантий; да и с ними может произойти так, что в коде закрался эффект, не видный из тестов (если они вообще есть), но влияющий на соседние компоненты во время выполнения программы. ООП ограничивает программиста, предлагая больше гарантий в сравнении с простым императивным кодом, но ООП остается императивным. ФП идет еще дальше и поднимает вопрос гарантий в плоскость математически обусловленных свойств кода, а математика, как известно, — самый сильный гарант гарантий.

Я бы мог продолжать и далее расписывать преимущества, но ведь это уже делали до меня сотни раз. Нет смысла об этом много читать, — это непродуктивно. Только экспериментом можно убедиться в истинности или лживости статей, подобных данной.

Что касается недостатков, они тоже есть. Вам придется платить тем, что вы уже не сможете просто фигачить фичи. А значит, какое-то время ваша продуктивность упадет, но качество кода начнет возрастать, — даже не на ФЯ. Разве вы никогда не занимались поддержкой жуткой лапши legacy, пришедшей из тех времен, когда практик еще не было? Иногда нет выбора, и поддержка legacy критична для бизнеса. Но сейчас совершенно нет никаких причин игнорировать практики при разработке новых больших проектов. Ведь выросла потребность и в командной разработке, а в ней понимание кода другими играет определяющую роль. Да, вы будете писать проект чуть медленнее. Но вы значительно выиграете время на его развитии и поддержке, не говоря уж о радикально сниженном количестве багов. ФП энфорсит корректность кода на многих уровнях, в отличие от того же ООП.

Я не буду говорить про производительность. Бегать с бешеными глазами и кричать о том, что теряется производительность, — это, ей-богу, ребячество. Как уже было много раз говорено, ваша задача вряд ли включает в себя требование hard или soft real time производительности, — а для всего остального ФП может подойти так или иначе. В конце концов, почему мы так необъективно забываем, что пишем на очень медленном Python? Все к нему привыкли? Но стоит заговорить о ФП, как производительность становится краеугольным камнем, на который давят все противники, даже питонисты. Но если разобраться, ФП не такое уж и медленное, а с учетом прекрасной поддержки параллелизма, оно еще и сильно выигрывает. Если бы мне предложили выбор с точки зрения производительности: Python или любой ФЯ, я бы не сомневаясь выбрал последнее.

Конечно же, изучение ФП требует времени. В ФП есть несколько уровней сложности, и эта тема более глубока, чем ООП (включая как классическое C++/Java ООП, так и исконное Алан Кеевское). Практики и подходы, паттерны проектирования и целые принципы построения дизайна (те же FRP или STM), — все это требует изучения, если вы начнете погружаться глубже и захотите перейти от кодера к человеку, мыслящему системно. Но пусть так: вам это не нужно, вы никогда не будете писать все в функциональном стиле, вам не нужно использовать ФП в повседневной разработке. Тем не менее, изучение даже базовых вещей из мира ФП сделает из вас более хорошего разработчика. И это на фоне того, что даже такие консервативные мастодонты как Java и C++ вносят в свой мир элементы ФП. Как вы думаете, почему это происходит?
Если вы пишете императивно на ФЯ, или пишете лапше-код — вы что-то делаете не так, и это не ФП.

Проблема в том, что если я не пишу императивно на ФЯ и не пишу лапше-код — я по-прежнему могу делать что-то не так, и это не будет ФП. Кроме того, вы даете неюзабельное определение: «это не ФП».

Кроме того, я вас не понимаю. С одной стороны, вы утверждаете, что в ФП нельзя нарушить SRP. С другой стороны, сразу же показываете как это сделать («если создать такую функцию, она не будет ни с чем компоноваться.»).

Что получается в итоге: «дробление задачи на маленькие кусочки; принцип единственной ответствености (SRP); принцип разделения ответственностей и выделение интерфейсов (ISP); Don't Repeat Yourself (DRY); и так далее» — это принципы применимые и в ООП. (Я исключил из вашего списка чистые функции, хотя они в ООП не запрещены). Лапше-код можно написать на любом языке. Подавляющее большинство задач можно сделать красиво, лаконично и неправильно как с ФП, так и с ООП. При этом ФП запрещает «фигачить фичи» (на самом деле нет), то есть обладает как минимум одним недостатком близким к фатальному.

По поводу производительности — да, ныне про неё можно забыть (хотя бенчмарки придумать сложно). 90% задач производительности не требуют.

В итоге из ваших комментариев не вполне ясны преимущества ФП. То есть, ограничения уже есть, а преимущества туманны.

P.S. Мы уже наваяли тонны текста. Предлагаю перенести дискуссию в личку.
Я дал юзабельное определение ФП в своем докладе, на который сослался в первом сообщении. Точнее, не определение, а набор признаков. Могу повторить здесь. Имеется три уровня понятия «Функциональное программирование»:

— Элементы ФП. Лямбды, иммутабельность, чистота, алгебраические типы данных, первоклассные функции и функции высших порядков. Это только элементы, а не всё ФП. Вы можете написать код с лямбдами и иммутабельностью, но он по-прежнему будет содержать шаги/инструкции вместо деклараций.

— Функциональный стиль. Здесь начинается настоящее ФП, а именно: композиция функций, каррирование, декларативность, частичное применение. В функциональном стиле можно писать чистый код и код с эффектами.

— Функциональный дизайн приложения. Системы типов, системы эффектов, предметно-ориентированные языки, ФПшные подходы, паттерны и идиомы. Вы строите приложение целиком в функциональном стиле и с применением best practices.

> С одной стороны, вы утверждаете, что в ФП нельзя нарушить SRP.
Нет, позвольте. «Нельзя нарушать» (в смысле — запрещено) и «нельзя нарушить» ( в смысле — невозможно) — разные вещи, и я написал верно. Сломать ФП-код можно, но не нужно.

Я не претендую на полное изложение преимуществ. Есть такое парадоксальное когнитивное искажение: чем больше приводится аргументов, тем оппонент менее склонен их воспринимать, и он неосознанно еще больше убеждается в своей правоте. Поэтому, возможно, статьи о преимуществах ФП выглядят для противников неубедительными. И поэтому я предлагаю ставить эксперименты.

ФП не запрещает фигачить фичи. Скорее, оно запрещает делать это неосознанно. Там, где кодер пойдет фигачить фичу, профессиональный разработчик подумает о всех крайних случаях, о требованиях, и прошерстит набор своих инструментов в поисках наиболее удовлетворяющего требованиям. В ФП это становится более важным, потому что писать наивный ФП код чревато многими неприятными последствиями.

Как я уже говорил, принципы универсальны. Но ООП не энфорсит их соблюдать, — да и само ООП может пониматься крайне различными способами. А когда вы начнете применять принципы к ООП коду — те же SOLID, — вы придете, внезапно, к ФП-коду, только перегруженному синтаксическими конструкциями из ООП мира. В ООП много ненужной мишуры, много ритуалов и синтаксических приседаний. Это все ведет к перегруженности кода, к тяжелому восприятию, к необходимости помнить о десятках элементов, не относящихся к предметной области, но используемых, чтобы соблюсти те или иные контракты. Возьмите хотя бы область видимости полей класса и модификаторы доступа. Это все — ненужный мусор, без которого можно обойтись, разделив ответственности на уровне модулей, и исключив глобальное мутабельное состояние. В итоге ФП код становится в гораздо большей степени про предметную область и бизнес-логику, чем про языковые конструкции. И это все ведет к снижению accidental complexity, — к снижению привнесенной сложности, что, в свою очередь, является главной задачей дизайна ПО.

P.S. Мне кажется, мы с вами хорошо беседуем. Я понимаю, что вам большая часть всего, что я говорю, хорошо известна. Поэтому я воспринимаю наш с вами диалог, скорее, как интервью, и рассказываю это все для потенциальных читателей.
Если говорить о SOLID, для меня неясно, как работает Dependency Inversion и Interface Segregation в ФП (и вообще, что есть аналог интерфейса в ФП). У меня впечатление, что ФП-шники на это не обращают внимание, концентрируясь на SRP из всего SOLID.

Есть ли у ФП-программы точка сборки (composition root)?

Может, есть статьи по этой теме?
Хороший вопрос. Начнем говорить о IoC/DI/ISP/интерфейсах с того, что есть два аспекта, а точнее, два вида полиморфизма:

— Динамический полиморфизм;
— Статический полиморфизм.

Динамический полиморфизм принят в «классическом» ООП, и это, пожалуй, самая главная его черта. На основе наследования можно разделить интерфейсы и реализацию, а в таких языках как Java и C# даже введены специальные синтаксические конструкции для этого. Динамический полиморфизм такого вида позволяет определить интерфейс в одном модуле трансляции, а реализацию — в другом модуле трансляции, и они могут быть скомпилированы раздельно. Мы пользуемся этим в подгружаемых библиотеках .NET и Java, реализуя предоставляемые ими интерфейсы.

Еще к динамическому полиморфизму можно отнести duck typing в динамических языках программирования. И с этим тоже не возникает проблем: мы реализуем inversion of control, используя duck typing. Это работает, например, в Python. Мы можем понимать класс как принадлежащий тому или иному интерфейсу, если в нем есть требуемые методы.

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

Статический полиморфизм работает иначе. В нем полиморфное поведение реализуется на уровне типов, когда есть некое обобщение типов, под которое попадают другие пользовательские типы. Генерики, шаблоны С++ и система типов Haskell — именно об этом. Полиморфный тип может пониматься как интерфейс для более конкретных типов. Функции высших порядков — те, что принимают другие функции на вход — могут пониматься как полиморфные, а их тип задает интерфейс. Таким образом, Inversion of Control зашит в ФП на уровне концепции, потому что в ФП вы передаете одни функции в другие, лишь бы тип принимающих функций (функций высших порядков) был достаточно общим для этого. Но у статического полиморфизма есть определенная проблема, доставляющая много неудобств в таких языках как Haskell, где система типов чрезвычайно строгая. Мы рассмотрим эту проблему чуть ниже, а сначала позвольте привести пример простого статического полиморфизма и оного же на основе классов типов.

Мы хотим взять список и вычислить какую-нибудь среднюю величину по всем элементам. Задача такого рода типична в мире ФП, поэтому она носит название «свертка». Например, свертка списка к сумме его элементов. Или к произведению. Или к другой структуре данных, например свертка списка натуральных чисел к списку четных чисел. Мы ограничимся только суммой. Функция свертки списка (в C++ — accumulate, в Haskell — fold, а есть еще reduce) ожидает на вход бинарный оператор, в нашем случае — оператор суммы (+). Функция свертки foldr в Haskell регламентирует то, что в нее нужно передать:
foldr :: (a -> b -> b) -> b -> [a] -> b

Здесь (a -> b -> b) — это бинарный оператор (например, лямбда суммы двух чисел), b — начальное значение (аккумулятор, у нас будет 0), а [a] — список из значений типа a. На выходе — значение типа b. Поскольку foldr полиморфен статически, на этапе компиляции в него будут подставлены наши конкретные типы, и получится такая неполиморфная функция:
foldr :: (Int -> Int -> Int) -> Int -> [Int] -> Int

Мы будем вызывать foldr с нашим оператором (+), имеющим как раз тип (Int -> Int -> Int), и это будет классический пример Inversion of Control, только не содержащий сторонних эффектов. А типы здесь выступают как интерфейсы.

Однако, статический полиморфизм более разнообразен, и классы типов (не путать с классами из ООП) позволяют реализовать IoC на еще более генеричном уровне. Приведенная выше функция foldr была полиморфной по оператору свертки, но она не была полиморфной по контейнеру, над которым производится сумма. Нам посчастливилось использовать список, а что если мы не знаем, какой у нас будет контейнер в будущем? Например, мы пишем библиотеку, а пользователи определяют свои коллекции? Тогда нам поможет еще более полиморфная функция:
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

И здесь уже появляется класс типов Foldable под сокращением t. Он говорит, что не важно, какая у вас коллекция t a, лишь бы для нее был реализован интерфейс свертки Foldable. Это может быть список, массив, граф, — что угодно. Класс типов здесь выступает интерфейсом для данных, хотя может выступать интерфейсом для поведения тоже. Примечательно, что в С++ ожидается нововведение под названием «концепты», которое является классами типов. И это нововведение сильно упростит многие библиотеки, включая Boost.

В статическом полиморфизме есть еще более высокие абстракции вроде семейств типов, и даже есть кривой аналог динамического полиморфизма в виде экзистенциальных типов. Однако, простой статический полиморфизм уровня типов имеет один существенный недостаток по сравнению с динамическим: нельзя определить пользовательский тип данных отдельно от интерфейса, приходится компилировать их вместе. Нельзя также передать скомпилированный пользовательский тип в рантайме, потому что принимающий клиентский код ничего про этот тип не знает, а система типов — строгая, и неизвестные типы не допускаются. Таким образом, подключаемые в рантайме плагины в Haskell — дело нетривиальное.

К счастью, имеется несколько других подходов, известных под общим названием «функциональные интерфейсы». Позвольте представить один из них, полный аналог интерфейсов в ООП: интерпретируемые языки. Суть в том, что для своей предметной области вы определяете свой язык с помощью типов, пишете клиентский код на этом языке, а потом «реализуете» ваш язык на этапе интерпретации. Меняя интерпретаторы, вы можете добиваться того, что ваш клиентский код — декларативные сценарии — будет исполняться по-разному.

Давайте рассмотрим эту тему подробнее. Обычно интерпретируемые языки создаются с помощью алгебраических типов данных (ADT). Конструкторы ADT будут вашими методами, а сам тип будет интерфейсом к вашей подсистеме. Очень упрощенный пример:

data AccountOperation
= PutMoney AccountNumber Money
| WithdrawMoney AccountNumber Money


Используя этот язык, вы пишете сценарии, которые зависят только от интерфейса. Это аналог клиентского кода, который использует интерфейсы в Java или C#. Разница лишь в том, что в ООП под интерфейсом скрыта конкретная реализация, а под интерпретируемым языком ничего не скрыто. На псевдокоде сценарий мог бы выглядеть так:

putAndGetMoney :: [AccountOperation]
putAndGetMoney =
[ PutMoney "1234" 100500
, WithdrawMoney "1234"
]

Вы пользуетесь декларациями методов вашего языка и определяете, что должно произойти. Но этот декларативный сценарий еще нужно заставить работать, — и здесь появляется этап интерпретации. Вы создаете интерпретаторы для различных условий, например, для разных банков. При этом интерпретаторы могут быть нечистыми, например — ходить в сторонние сервисы или в БД. Вот как мог бы выглядеть интерпретатор:

interpret :: AccountOperation -> InputOutput
interpret (PutMoney number amount) = SberbankAPI.put number amount
interpret (WithdrawMoney number amount) = SberbankAPI.withdraw number amount


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

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

Кроме того, существует ряд других способов сделать DI в строго функциональном стиле. Я не буду вдаваться в подробности, а отправлю вас к своей (недописанной) книге, где я посвятил этому целую четвертую главу. Материал доступен онлайн: Functional Design and Architecture.

А про точку сборки я не имею представления, так как сталкиваюсь с понятием впервые. Либо позвольте мне сперва изучить вопрос, либо расскажите, что это в мире императивного программирования.
про точку сборки я не имею представления, так как сталкиваюсь с понятием впервые. Либо позвольте мне сперва изучить вопрос, либо расскажите, что это в мире императивного программирования

DI требует, чтобы классы зависели от абстракций, а не от других классов.
Для этого, вместо зависимого класса используют интерфейс, а экземпляр зависимого класса создаётся где-то снаружи и обычно передаётся через конструктор:
public class SberbankService : IBankService {
   public SberbankService(IHttpClient htppLib) { ... }
}

Так вот, точка сборки — это место в коде (выполняющееся обычно при старте приложения), где всём абстракциям назначаются реализации:
IHttpClient http = new HttpLib();
IBankService bank1 = new SberbankService(http);
IBankService bank2 = new AlphabankService(http);
IBankService loggedBank2 = new GenericLogger<IBankService>(bank2);
ITransferService tr = new TransferService(bank1, /*bank2*/ loggedBank2);
...
tr.MoveMoney(acc1, acc2, 100500);

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

С учётом написаного вами выше, понятно, что такое можно сделать и в ФП, если передавать во все функции первыми параметрами зависимые функции, а далее делать частичное применение, фиксируя первые параметры.

Вопрос тогда трансформируется в следующий: а в ФП-проектах такое в принципе практикуется
1. все композиции фунцкций делать в одном месте
2. все функции бизнес-логики никогда не вызывать напрямую, а принимать их как дополнительный параметр, и вызывать только через принятый параметр.
в п.2 особый акцент на слове *все*, т.к. если где-то забыл об этом принципе, считай, что DI сломана.

И ещё мне показалось (это не проблема ФП в целом, а проблема Haskell), что все эти Int->(Int->Int->Int)->Int добавляют когнитивной нагрузки при написании и чтении кода. Тут будет придерживаться SOLID только потому, что иначе любое описание функции утонет в 50 стрелочках. Причём в ООП это будет код с душком, но в принципе поддерживаемый.

С интерпретаторами как-то очень сложно.
Даже на простом примере без них нельзя?
Вот, к примеру есть такая абстракция:
interface ISummator { int sum(a,b); }
interface IMultiplier { int mul(a,b); }
int dotProduct(ISummator s, IMultiplier m, Point x, Point y);


Можно сделать
dotProduct :: (int->int->int)->(int->int->int)->p->p->int
Но как защититься от неверного использования, когда вместо summator передали multiplier, и наоборот.

Можно ли к (int->int->int) сделать какую-то метку, чтобы туда подходили не все функции, а только специально помеченные, как совместимые на это место?
Спасибо за разъяснение. Значит, то место, где я заполнял IoC контейнеры, имеет свое название — «точка сборки». В ФП-программе мы делаем точно так же. В главной функции main мы определяем конкретные реализации и передаем их как зависимости в клиентский код. И да, мы это делаем в настоящем проекте, не в придуманном.

Я попробую ответить на ваш вопрос, хотя не очень понимаю словосочетание «все композиции в одном месте». Если бы мы заменили слово «композиции» на «настройку» или «инстанцирование», — было бы лучше?

Чтобы количество стрелочек в аннотации типа не превышало все разумные пределы, мы воспользуемся алгебраическим типом данных. Пусть этот тип содержит все возможные зависимости, то есть, он будет являться аналогом IoC-контейнера. Я назову этот тип Runtime:
data Runtime = Runtime
    { summator :: Int -> Int -> Int
    , multiplier :: Int -> Int -> Int
    }


Полей для разных зависимостей может быть сколько вам нужно. Настроить Runtime мы можем, например, так:

main = do
   let runtime = Runtime {summator = (+), multiplier = (*)}
   dotProduct runtime (Point 1 2) (Point 2 3)


Передаем этот «контейнер» как аргумент, и все его содержимое становится доступно функции:
dotProduct :: Runtime -> Point -> Point -> Int
dotProduct (Runtime sum mul) p1 p2 = mul (sum 3 5) 10
-- result: 80


Вы спрашиваете, как пометить (Int -> Int -> Int). Конечно же, это можно сделать, — и несколькими способами. Начнем с алгебраических типов-оберток, в которые положим функции суммирования и умножения:

data Summator = Summator (Int -> Int -> Int)
data Multiplier = Multiplier (Int -> Int -> Int)

dotProduct :: Summator -> Multiplier -> Point -> Point -> Int
dotProduct (Summator sum) (Multiplier mul) p1 p2 = mul (sum 3 5) 10


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

data BinOperation
   = Sum Int Int
   | Mult Int Int

type Interpreter = BinOperation -> Int
interpret :: Interpreter
interpret (Sum x y) = x + y
interpret (Mul x y) = x * y

dotProduct :: Interpreter -> Point -> Point -> Int
dotProduct eval p1 p2 = eval (Mul (eval (Sum 3 5)) 10)


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

Я еще не рассказал, как можно для тех же целей использовать классы типов, Free monads и обобщенные алгебраические типы данных (GADTs). Если желаете погрузиться в Haskell еще больше, — расскажу :)
Я немного поторопился.
> только функции будут уже нечистыми, как в нашем примере.
Не как в нашем примере.
Спасибо за разъяснение. Значит, то место имеет свое название — «точка сборки»
В книге Симана Марка «Внедрение зависимостей в .NET» можно прочесть:
Слабое связывание позволяет писать код, который допускает расширяемость, но не позволяет вносить изменения. Такой подход называется принципом открытости/закрытости (Open/Closed Principle). Единственное место, требующее модификации кода, — это стартовая точка приложения, называемая корнем сборки (Composition Root)
«Точка сборки» — вероятно, хабражаргон, который я встречал в нескольких статьях (например) и во многих комментариях.

Что меня озадачило, так это фраза «место, где я заполнял IoC контейнеры».

В моём примере выше composition root написан вручную, без IoC-контейнера. Контейнер же — это довольно сложная библиотека, которой скармливаешь пары [интерфейс, реализация] и дополнительную информацию (время жизни, стратегия создания экземпляра) и т.п., а эта библиотека, когда её попросишь создать какой-то экземпляр, автоматически строит граф зависимостей и создаёт экземпляр и все зависимости в соответствии с настройками.

Обычно контейнер использует рефлексию, поэтому я очень удивился, обнаружив упоминание контейнера в контексте ФП.
Я назову этот тип Runtime
Блин, вот что значит зацикленность на стереотипах. АТД я мыслил типа как record в Паскале. А то, что можно описать функцию, вместо привычных полей с данными, как-то даже и не подумал. Тогда вообще никаких проблем с интерфейсами.

Но это не вполне убедительно, так как функции sum и mul, лежащие в алгебраических типах данных, все равно можно спутать в клиентском коде.
Попробую по-другому сформулировать, что я хочу. Например, у меня есть 5 функций sum1, sum2, sum3, sum3, sum4, sum5 и 3 функции mul1, mul2, mul3 с одинаковым типом int->int->int. Можно ли написать такую ф-цию
dotProduct :: X->Y->Point->Point->int
(подставив вместо X и Y какие-то ограничения), чтобы компилировался вызов, только если первый параметр dotProduct — одна из ф-ций sum1-sum5, а второй параметр — mul1-mul3?

Попробуем сделать так, чтобы этого не случилось. Сделаем интерпретируемый язык.
Интерпретатор, как я понимаю, не позволит в dotProduct перепутать Sum и Mul. Но мы этого уже добились, введя тип Runtime, и присвоив правильное значение переменной runtime. Что меняется с вводом интерпретатора, для этого примера он избыточен?

По интерпретаторам интересно узнать:
1. Интерпретация — это медленно?
2. Насколько громоздко описывать интерпретаторы? Верно ли, что выше приведён кусочек, иллюстрирующий идею, а для запуска надо ещё написать много-много строчек?
Да, назвать мою структуру данных аналогом IoC-контейнера было несколько опрометчиво.
Тогда вообще никаких проблем с интерфейсами.

Прошу заметить, передача функций как зависимостей в другие функции имеет одно существенное отличие от интерфейсов в ООП: под функциями не скрывается никакого объекта, который бы мог содержать состояние. И если понимать мой Summator как внешний сервис, то этот сервис будет stateless. Можно ли сделать полную аналогию ООП-интерфейсов на функциональных языках? Если говорить о Haskell, то трюк с экзистенциальными классами типов может быть здесь полезен. Я рассказывал об этом в своей статье «Дизайн и архитектура в ФП», и за деталями отправляю вас к ней. Здесь же скажу, что пытаясь скопировать интерфейсы ООП, мы некоторым образом загоняем себя в угол, лишаем себя всех преимуществ ФП и получаем недостатки ООП. Мой код в приведенной выше статье оказался плохо компонуемым, перегруженным, неудобным в использованни, и в целом он не вписывался в остальную часть приложения. Конечно, это может быть только проблема Haskell, а в Scala ее нет вовсе, поскольку ее философия — в объединении ООП и ФП. Но мы еще не вполне понимаем, как балансировать между двух философий, у нас еще не выработаны практики, мы еще экспериментируем с вариантами дизайна ООП+ФП.

(подставив вместо X и Y какие-то ограничения), чтобы компилировался вызов, только если первый параметр dotProduct — одна из ф-ций sum1-sum5, а второй параметр — mul1-mul3?

Типы-обертки (Summator, Multiplier) вполне решают эту проблему. Их могут неверно проинициализировать, — да, однако, это уже проблема вызывающей стороны. Но если данный подход не вполне вас убеждает, позвольте предложить вам другой: классы типов.

Идея в том, что мы не будем передавать операцию напрямую. Вместо этого, мы будем передавать специальный «тип-провайдер», а потом узнавать, какая операция с ним связана. Операции с типами мы связываем через классы типов, которых у нас будет два: SumProvider и MulProvider. По сути, класс типов можно понимать как интерфейс с набором stateless методов, а его реализациями будут конкретные пользовательские типы.

Класс типов для сумматоров будет таким:
class SumProvider t where
    sum :: t -> Int -> Int -> Int

Теперь создадим два алгебраических типа для двух разных операций суммирования, они будут крайне тривиальными:
data Summator1 = S1
data Summator2 = S2

Нам важен сам факт, что мы используем этот тип-провайдер. Класть мы в него ничего не будем. Но нужно связать с каждым свою операцию суммирования. Это делается при помощи инстанцирования класса типов:
instance SumProvider Summator1 where
    sum S1 x y = x + y

instance SumProvider Summator2 where
    sum S2 x y = x + y + 100

Здесь конструкторы S1 и S2 нужны просто для индикации. При необходимости, мы бы могли хранить в них полезную информацию. Давайте так и сделаем с мультипликатором: положим в конструктор множитель. Вот так будет выглядеть тип-провайдер:
data Multiplier1 = M1 Int

А вот это — класс типов и его инстанцирование для Multiplier1:
class MulProvider t where
    mul :: t -> Int -> Int -> Int

instance MulProvider Multiplier1 where
    mul (M1 coeff) x y = x * y * coeff


Наконец, два последних штриха. Функция dotProduct уже принимает не функции (int -> int -> int), как это было ранее, а типы. Только мы говорим ей, что нам подойдут не любые типы с улицы, но те, которые мы сделали «провайдерами». То есть, они точно связаны с классами типов, иначе не скомпилится. Этот факт мы отражаем следующей синтаксической конструкцией:

dotProduct :: (SumProvider sp, MulProvider mp) => sp -> mp -> Point -> Point -> Int
dotProduct s m x y = mul m (sum s 3 5) 10


Здесь sp и mp — наши конкретные типы, про которые известо только то, что они принадлежат SumProvider и MulProvider соответственно. А раз так, мы можем вызвать функции sum и mul с их конструкторами s и m. Тогда выберется конкретная реализация функции sum или mul.

Вот как мы используем этот код:
main = do
    let result1 = dotProduct S1 (M1 1) (Point 0 0) (Point 0 0)
    let result2 = dotProduct S2 (M1 2) (Point 0 0) (Point 0 0)
    
    print result1   -- 80
    print result2   -- 2060


Я написал для вас сниппет, вы можете испытать его онлайн здесь.

Что меняется с вводом интерпретатора, для этого примера он избыточен?

Для этого примера и правда подход с интерпретируемыми языками избыточен. Ситуация меняется, когда у вас есть подсистемы или внешний API. Есть способы создать встроенный предметно-ориентированный язык (eDSL), который будет исключительно прост в использовании, но при этом будет оперировать функциональными интерфейсами, а не их реализацией. Это позволяет, например, обобщить операции с банками, скрыв реализацию конкретного API в слое интерпретации. В таких языках методов может быть значительно больше, чем два. Для примера взгляните на вот этот язык разработки мобильных приложений, который я создал для одной индийской компании (ключевая часть — алгебраический тип FlowMethodF). Это — интерпретируемый язык на Free-монадах. Полагаю, выглядит устрашающе? Прошу учесть, что это не просто интерпретируемый алгебраический тип данных. Это интерпретируемый АТД, завернутый во Free-монаду. Поэтому используются дополнительные инструменты и функции, чтобы заставить это все работать. К счастью, сложность машинерии не увеличивает сложность самих сценариев, потому что одна из главных целей Free-монад — сделать сценарии как можно чище и избавить их от всего, что не относится к предметной области. Писать сценарии у нас могли даже менеджеры, которые никогда не изучали Haskell. Вот как выглядит совсем простой сценарий:
-- Presents UI to user, waits to action and recurse.
count :: Int -> Flow Unit
count val = do
  Increment <- runUI $ CounterScreen val
  count $ val + 1

Обратите внимание, что в этом примере есть точка сборки, где мы инициализируем Runtime.
Имеются также два интерпретатора: для реального мира и для тестов. Код, возможно, выглядит сложно, но на самом деле, это уровень middle haskeller, и в целом, приведенный код не так страшен. Кроме Free-монад есть и другие подходы (GADTs), где интерпретаторы устроены гораздо проще.

Давайте поговорим о производительности итерпретаторов. У Free-монад есть свои особенности, и по сравнению с моими примерами интерпретируемых ADT в предыдущих сообщениях, Free-монадные языки будут медленнее. Однако наши реальные мобильные приложения, построенные на интерпретируемом Free-монадическом языке Flow, ведут себя вполне отзывчиво и не лагают. Более того, у нас в этой компании есть еще один проект: сервис распределенных workflow, построенный по той же схеме Free-монад. Workflow-сценарии в нем подгружаются в рантайме и исполняются на сервере. Производительность его сравнима с производительностью Node.JS, на котором сервис и крутится. Поэтому про производительность я бы сказал, что она достаточна.
Спасибо за подготовленный пример, я с ним немного поэкспериментировал, и этот подход мне нравится.

Буду дочитывать примеры и вникать.

Интересно ваше мнение насчёт замыканий. Замыкания хранят состояния (и на этом строят генераторы). Значит, они не чистые и их стоит избегать?
Был рад помочь :)

Интересно ваше мнение насчёт замыканий. Замыкания хранят состояния (и на этом строят генераторы). Значит, они не чистые и их стоит избегать?

Интересный вопрос, я в таком ключе не думал. Ответ будет такой: замыкания — это прекрасно, избегать их не нужно, ведь они иммутабельны. Попробую пояснить, но мне придется начать немного издалека, чтобы самому осознать, чем являются замыкания в данном контексте.

Состояние в ФП имеется: чистое иммутабельное и нечистое мутабельное.

Нечистое мутабельное состояние может храниться в объектах и может быть изменено. В Haskell есть, например, IORef, в который можно положить что угодно, но только в нечистой части. В чистой части с состоянием внутри IORef сделать ничего нельзя.

Чистое иммутабельное состояние передается по цепочке трансформаций. При этом замыкания — это тоже состояние, пришедшее из внешней области видимости, от внешних функций. Но можно считать, что оно передается, а не хранится, так как у нас нет возможности поменять ту или иную переменную. Вот пример:

myFunc :: Int -> Int -> (Int -> Int)
myFunc x y = (\z -> x + y + z)

callMyFunc x = (myFunc x 2) 3


Переменные x и y попали в замыкание, но для внешней функции ничего особо не изменилось, потому что они все равно передаются в аргументах.

На таком «чистом» состоянии основывается монада State. Эта монада, на самом деле, лишь оборачивает передачу аргументов в монадический синтаксис. Видя код в монаде State, можно подумать, что состояние хранится где-то в бэкграунде, и что мы его изменяем. Пример:

incrementCounter :: State Int
incrementCounter = do
    prevValue <- get
    set (prevValue + 1)

updateCounter :: State Int
updateCounter = do
    incrementCounter
    incrementCounter
    incrementCounter

main = do
    let result = evalState updateCounter 0
    print result  -- 3


Но на самом деле, хранимого состояния здесь нет. «Под капотом» do-блоков монада State просто передает наш счетчик дальше по цепочке наших функций. Сломать ничего нельзя: если мы обновляем счетчик, мы его копируем, а не затираем предыдущее значение.

Поэтому и замыкания, и чисто функциональное состояние — отличные инструменты.
Хотел я попробовать сломать State, но видимо, в онлайн-компилятор не доставили нужные модули
rextester.com/live/CPQ26566
source_file.hs:3:8:
Could not find module ‘Control.Monad.State’
Perhaps you meant
Control.Monad.ST (from base-4.8.2.0)
Control.Monad.ST.Safe (from base-4.8.2.0)
Control.Monad.Fix (from base-4.8.2.0)
Это из пакета mtl, для навигации можете использовать stackage.org.

do-нотация — это просто синтаксический сахар для композиции монад. Когда мы работаем с обычной State-монадой, мы имеем пару «текущий стейт» + «вычисляемое значение» (не знаю правильный ли термин), и передаём в следующую функцию (монаду), в ней можем поменять стейт, оставить его как есть, можно повлиять на вычисляемое значение исходя из полученного от предыдущей функции (монады) стейта. Это можно записать и объяснить каким-нибудь псевдо-кодом, тогда станет понятно, почему стейт-монада «чистая», несмотря на внешние сходства do-нотации на императивный стиль.
Напр. этот код:
incrementCounter :: State Int ()
incrementCounter = do
  prevValue <- get
  put (prevValue + 1)

updateCounter :: State Int ()
updateCounter = do
  incrementCounter
  incrementCounter
  incrementCounter

Можно заменить на:
incrementCounter :: State Int ()
incrementCounter =
  get >>= \prevValue -> put (prevValue + 1)
-- или point-free
-- incrementCounter = get >>= put . (+ 1)

updateCounter :: State Int ()
updateCounter =
  incrementCounter >> incrementCounter >> incrementCounter
Определения — ок.

Сломать ФП-код можно, но не нужно.

Согласен. Тот же подход можно применить и к ООП.

Есть такое парадоксальное когнитивное искажение: чем больше приводится аргументов, тем оппонент менее склонен их воспринимать.

Согласен. Стараюсь быть аккуратнее (вроде взаимно). Следствие: нет резона объяснять ФП в статьях? Какой формат может быть лучше?

Там, где кодер пойдет фигачить фичу, профессиональный разработчик подумает о всех крайних случаях, о требованиях, и прошерстит набор своих инструментов в поисках наиболее удовлетворяющего требованиям.
Аналогично для ООП.

В ФП это становится более важным, потому что писать наивный ФП код чревато многими неприятными последствиями.
То есть, ООП безопаснее-по-дефолту? Меньше порог входа?

Возьмите хотя бы область видимости полей класса и модификаторы доступа. Это все — ненужный мусор...
Спорно. Трудно доказуемо. Кроме того, модификаторы доступа — один из первых элементов ООП (по крайней мере, один и самых употребительных). Безотносительно истинности аргумента — его тяжело принять.
> Следствие: нет резона объяснять ФП в статьях? Какой формат может быть лучше?

Будем рассматривать это следствие для комментариев и комментирующих. В статьях, конечно же, говорить об этом нужно. А в ленивому комментатору, возможно, проще сделать ссылку.

Я не уверен, что значит «безопаснее по-дефолту». Мне также трудно оценить порог входа в ООП. Есть разные уровни сложности и в ООП, и в ФП. Новичок не сможет задизайнить систему хорошо ни там, ни там. Ему придется изучить как философию, так и подходы, принятые в парадигме. Есть свидетельства, что в ФП материала больше на всех уровнях, но его понимание и освоение может протекать также гладко, как и для ООП. У меня есть примеры, когда вчерашние студенты через две недели изучения уже начинали писать код на Haskell. Пусть простой, но все-таки функциональный. Мне сдается, что сложность ФП преувеличена. И об этом я тоже говорил в докладе.

> Спорно. Трудно доказуемо. Кроме того, модификаторы доступа — один из первых элементов ООП (по крайней мере, один и самых употребительных). Безотносительно истинности аргумента — его тяжело принять.

Не проблема, пусть останутся моменты, на которых мы расходимся.
Как инженер, не согласен со многими высказываниями.
Самый яркий пример само-ограничений (для c, c++), который я знаю-MISRA. Кровавые слёзы промышленного программирования так сказать.
По мне — так ограничения необходимы лишь для того, чтобы упрощать себе жизнь.

Скажем, в каждый момент времени программе должны быть доступны только те данные, с которыми идет работа. Это автоматически устраняет ошибки, связанные с изменением данных, которые изменяться не должны.

Или, скажем, константные функции нужны, чтобы нельзя было случайно внести изменения в объект.

То есть, маловероятные возможности доступа жертвуются в пользу уменьшения вероятности ошибок, что по-моему, очень даже разумно.

Вводная часть статьи, где говорится про ограничения — передергивания и фактические ошибки. Неконструктивно :)


Аналогия с музыкой.


То есть ограничения могут делать предмет более притягательным.

(А могут и не сделать)
В программировании не стоит задача сделать код притягательным. Код должен быть понятным. Поэтому применимо к программированию "всё остальное до отвращения очевидно" явно является предпочтительным вариантом.


Аналогия с фильмом Челюсти.
Это аналогия того же уровня, что и предыдущая. Что хорошо в кино, необязательно хорошо в разработке ПО. Как говорится, "плохая аналогия подобна котенку с дверцей".


Итак, ограничения делают всё лучше. Гораздо лучше.

Явное передергивание. Не все, а только вышеуказанные фильм и песню. И то не факт, что именно ограничения, а не, допустим, талант авторов.


Пример с художником.
Опять же, некорректная аналогия. В разработке редко стоит вопрос "что сделать". Вопрос стоит "как сделать".
Можно привести контр-пример. Заказчик говорит: "Мне нужен такой-то продукт, используй для него PHP". PHP — ограничение. Делает ли оно решение задачи проще? Может быть — если PHP действительно хорошо подходит для этой конкретной задачи. А если не очень, то это ограничение только мешает.


Работа с "железом".


ограничения делают работу с аппаратным обеспечением проще, чем работа с ПО

Ну да, ну да. Физические ограничения, наоброт, все усложняют. Борешься с одним ограничением — оп! уперся в другое. Насколько было бы проще разработать процессор, если убрать, скажем, ограничение по температуре / теплоотводу.
Ну или я просто совсем не понял мысль автора в этом пункте.


В программах можно все.
(1)


никакие ограничения не накладываются на то, что она может делать с принадлежащими ей кодом и данными.

(2)


Именно отсутствие ограничений делает написание и поддержку ПО таким сложным делом.

Отсутствует аргументация для перехода от (1) к (2).
Я так же безапелляционно могу заявить, например:
"Программы пишутся людьми. Именно человеческий фактор делает написание и поддержку ПО таким сложным делом."
Или же
"Программы моделируют реальный мир. Именно разница между реальным миром и машинной моделью делает написание и поддержку ПО таким сложным делом."
Или даже
"Программы работают с данными. Именно это делает написание и поддержку ПО таким сложным делом".
Или… ну, вы поняли.


Пример с GOTO, который "ограничили".


Даже в этом примере, видно, что на самом деле индустрия выиграла не от введения ограничений (как уже писали выше, goto все так же присутствует во многих языках), а от добавления новых возможностей (напр., явные управляющие конструкции для циклов).
Да банально взять любой ЯП и посмотреть, как он развивался со временем. В большинстве случаев это добавление новых возможностей, а не постановка дополнительных ограничений. Неужели это делает ЯП хуже, а написание програм сложнее?


Ну, а если вводную часть про ограничения убрать из статьи, то на выходе получится очередная одна статья из разряда "ФП — лучше всех!" :)

Скажу сразу, что статья мне в общем понравилась. Но что меня смущает, так это то, что автор как-то уж очень утрированно подходит ко многим вопросам.

Например про глобальное состояние и перезагрузки. Само по себе глобальное состояние это и не плохо и не хорошо. Все зависит только от качества реализации. Например я сейчас работаю с системой, софт для которой написан на языке, у которого _все_ переменные глобальные. И на этом языке написана большая часть как системного так и пользовательского ПО. Возможно вы подумаете, что эта система только и делает, что перезагружается? Но она рассчитана на то, что бы не перезагружаться вообще никогда. И это совсем не значит, что ничего нельзя менять. Можно. Прямо в работающих модулях. Налету. Вы грузите нужные изменения, а модуль продолжает выполнять работу. Причем не важно это модуль системный или пользовательский. Да, бывает, что в системе обнаруживаются ошибки. Но это ни коим образом не влияет на работу всей системы. Система модульная. И для каждого процесса собирается цепочка необходимых модулей. Эта цепочка выполняется изолированно. И если что-то пошло не так, то это может повлиять только на текущий процесс. И что бы был понятен масштаб бедствия система может выполнять сотни тысяч таких цепочек. И что бы окончательно уверить вас в том, что главное- ровные руки, скажу, что это еще и система реального времени.
Тут важный момент — любой ли модуль может получить доступ к любой глобальной переменной?

А то ведь загрузится модуль, попортит данные всем другим модулям, и дальше что делать? Заменить один этот модуль на исправленный недостаточно. Только систему перезагружать
Да. Любой модуль имеет доступ к любым переменным. Пользователь с достаточными правами в системе может прочитать или установить любую переменную.

С какой это стати любой из модулей будет портить данные? Вы в своих программах портите «чужие» данные просто потому, что можете?

И вы были невнимательны. Модуль менять не нужно. Изменения вносятся в работающий модуль.

И нет. Систему перезагружать не нужно.

Мне кажется, что именно функциональный подход накладывает больше вычислительных и аппаратных ресурсов нежели ООП-шный, поправьте если я ошибаюсь.


К примеру, приложение типа Todo состояние, которого представлен в виде массива из 50 записей, и вот мне надо одну строчку пометить как "выполнено", мне необходимо сделать новую копию 50 записей, заменив значение на true, то самой строчки.


Разве это оптимальное решение, о котором говорится в последнем пункте статьи? Тут затраты процессора на перебор, тут память выделяется под новые записи и только для того, чтобы поддержать иммутабельность состояния приложения. Вот у меня операционка заняла память 4ГБ, и надо поменять какое-то состояние и ...


Еще один момент, в статье говорится, что глобальное состояние это зло, но в функциональном программирование, состояние это один глобальный стор (я тут сравниваю Редакс), когда в ООП-шном подходе состояние раздроблено и спрятано в компоненты/модели — одним словом в объекты и нет никаких глобальных состояний.


Чего-то я не до понимаю, хотя очень стараюсь, не получается вникнуть в эту новую философию программирования.

«Я сделяль...» :)

data Task = Task { t_id::Int
                 , title::String
                 , text::String
                 , is_finished::Bool
                 } deriving Show

myTasks = [ Task 0 "Sleep" "Have a sleep" True
          , Task 1 "Work" "Make some work" False
          , Task 2 "Fap on Haskell" "..." False
          , Task 3 "Something else" "..." True
          ]

setFinished :: Int -> [Task] -> [Task]
setFinished task_id = map (\t@(Task tsk_id ttl txt _) ->
                               if t_id t == task_id
                               then Task tsk_id ttl txt True
                               else t)

-- set task as finished (returns new list with tasks)
setFinished 2 myTasks


Я тоже начинающий, но, надеюсь, меня за этот пример не забьют палками.
Так вот, его суть в том, что мы задаём тип данных Task, который имеет «поля» для хранения id, названия, текста и завершённости задания. Потом создаём список myTasks с четырьмя заданиями.

Функция setFinished — это самая примитивная функция, которая выполняет то, что Вы хотите — «помечает» задание с требуемым id как «выполненное» (в данном случае вызов «setFinished 2 myTasks» возвращает новый список задач, где задача 2 выполнена). Как видите (ну или не видите, что тоже весьма вероятно XD) мы не делаем копии всех имеющихся записей. Мы только пересобираем основной список (N элементов) и ту запись, которую нам нужно изменить. Остальные, насколько я понимаю, остаются нетронутыми.

Для сравнения можете представить, что у вас есть односвязный список в С++ (например), каждый узел которого хранит указатель на объект типа Task. Даже при необходимости соблюдения иммутабельности объектов, Вы не будете делать копии «всех 50 записей» — Вы просто скопируете указатели 49 записей в новые узлы списка и создадите новый объект изменяемой записи.

Как-то так, в общем… Надеюсь, всё правильно расписал.
for-ы break-и иной раз куда более витиеватыми выходят, чем простой goto.
Sign up to leave a comment.

Articles