Почему функциональное программирование такое сложное

  • Tutorial

Я несколько раз начинал читать статьи из серии «Введение в функциональное программирование», «Введение в Теорию Категорий» и даже «Введение в Лямбда Исчисление». Причем и на русском, и на английском. Каждый раз впечатление было очень сходным: во-первых, много новых непонятных слов; во-вторых, много новых определений, которые возникают из ниоткуда; в-третьих, совершенно непонятно, как это использовать.


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


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


Кому эта статья


Эта статья для программистов, давно желавших понять Функциональное Программирование, пытавшихся что-то почитать на эту тему и упершихся в стену «да что, это блин за хрень такая, и зачем все так усложнять!?». Поэтому в этой статье я попытаюсь ответить на вопрос «зачем они это придумали», не сильно ударяясь в технические дебри. Я сегодня побуду таким «Робертом Киосаки от функционального программирования», который не столько учит вас финансовой функциональной грамотности, сколько мотивирует ее в себе развивать.


Дисклеймер


Я не претендую на звание эксперта в функциональном программировании или Теории Категорий. Далее в статье я излагаю довольно упрощенный и частный взгляд на довольно нетривиальные вещи. Прошу отнестись с пониманием к неточностям и ошибкам, ведь даже «истина, высказанная словами – есть ложь». Тем не менее я буду рад уточнениям и исправлениям, отправленным в личку.


Зачем нам Функциональное Программирование?


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


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


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


Что я понял по результатам моих попыток.


Есть общая беда всех курсов из разряда «Введение в …» (даже со смешными картинками) – они дают пачку базовых определений, постепенно повышая сложность. «Почему это беда?», спросите вы. Отвечу аналогией: когда вы учите двухлетнего ребенка отличать круглое от квадратного, вы не даете ему геометрической аксиоматики, «что такое прямая», «что такое окружность» и т.п. плавно подводя к определению «шар» и «куб». Вы просто даете ему кубики и шарики и доску с дырками, в которую их надо вставить. Только значительно позже, уже в средних классах, он узнает их формальные определения. Конечно, программист с опытом – не двухлетний ребенок. Но встроенная в каждого человека нейросеть эволюционировала как инструмент обобщения разрозненных примеров в общий абстрактный принцип, а совсем не как инструмент, получающий готовые абстрактные принципы. Обучение на примерах всегда идет быстрее нежели сухое перечисление определений.


Анекдот:


– Пап, а как пишется восьмерка?

– Как "бесконечность", повернутая на пи-пополам».

Аналогично, при чтении очередного определения «монады» через функтор, у меня в голове возникала только одна мысль: вроде все понятно, но непонятно НАХРЕНА. Ощущения примерно соответствуют этой картинке:



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


Сначала немного бла-бла


Так уж исторически сложилось, что основная терминология ФП пришла из мира математики. Причем ОЧЕНЬ абстрактной математики. Теория Категорий, разработанная в 1940-х годах – это настолько абстрактная теория, что она полностью оторвана не только от реального мира, но и от многих разделов «обычной» математики. По своей абстрактности она близка к формальной логике «на стероидах».


Хорошая новость состоит в том, что для того, чтобы понять ФП совсем не обязательно начинать (или вообще знать) Теорию Категорий. Я советую сначала разобраться в практике ФП, а потом уже копать в сторону «корней» теоретической основы.


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


Абстрактность


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

(с) бессмертное


Следуя подходу Теории Категорий «обобщай пока есть что обобщать», в ФП обобщают все что можно. Если что-то можно обобщить или абстрагировать – оно будет обобщено и абстрагировано. В итоге все приходит к условной абсолютно абстрактной «монаде», которая как «Многоликий Будда» не может быть описана одним предложением (хотя ниже я попытаюсь).



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


  • Если ФП-программист видит две функции с циклами внутри, то он пишет одну библиотечную функцию, которая реализует абстракцию «цикл» с параметром «тело цикла». Заодно делая ее таким образом, чтобы вложенные циклы выглядели как параметр для параметра «тело». И т.п.
  • Если ФП-программист видит два оператора if, то он пишет функцию, которая принимает предикат и возращает монаду, превращая весь код в цепочку вызовов функций map.

Если у тебя из инструментов есть только функции, то рано или поздно все начинает казаться монадой.

(с) мое.


Декларативность


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


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


Решаем задачу Императивно Декларативно
Найти клад Если на улице дождь, надень куртку, выйди из дома, пройти 100 шагов на север, 200 на восток, возьми лопату, копай пока не наткнешься на клад Достань подходящим инструментом из земли клад прибыв на координаты (Lat, Lon) в подходящей одежде.
Сварить борщ Помой морковь, почисти лук. Обжарь их на сковороде в подсолнечном масле. Свари бульон на мясе, вынь мясо, положи туда обжаренные лук и морковь. Порежь картофель и свеклу, брось в бульон. Вари 20 минут. Вари до готовности содержимое кастрюли, наполненной бульоном из-под сварившегося мяса, в которую положил порезанные и чищенные картофель со свеклой и обжаренные в подсолнечном масле порезанные чистый лук и морковь.

Видно, что наши «программы» отличаются «точкой зрения на проблему». Императивный подход – постепенно конструируем результат от простого к сложному. Первая написанная функция императивного программиста будет «Чистить(Овощ) = …». Декларативный – наоборот: начинаем с самого конца, постепенно декомпозируя задачу на более мелкие. Первая написанная функция будет называться «ГотовыйБорщ = …».


Но есть еще более существенная разница. Дело в том, что императивная версия программы по поиску клада выполнится корректно только для конкретной начальной точки (хоть метр в сторону – и все было напрасно). А если в процессе варки борща, окажется что нет свеклы, то мало того, что борщ не сварится, так еще и зря пропадут уже порезанные продукты. А при попытке перезапуска процесса варки морковь окажется порезанной дважды. Поэтому основная проблема императивного подхода – большая чувствительность к начальному состоянию и прочим глобальным переменным в процессе исполнения. Что приходится компенсировать бесконечными if-ами, assert-ами, и обмазывать толстым слоем контрактов и тестов.



Может показаться, что я "натягиваю сову на глобус", выставляя декларативное программирование святым граалем. Но истина в том, что за все приходится платить. Основной недостаток Декларативного подхода – это необходимость прописывания всех (вот прям вообще всех, Наташ!) ограничений на все данные на всех этапах обработки – еще до запуска, что повышает сложность на этапе программирования, но отплачивает сполна в процессе работы. Этот полу-магический эффект, «если скомпилировалось, значит работает», замечают практически все, кто изучает ФП. Плюс функциональный код легко распараллеливается.


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


Чистая функциональная программа – это не поток исполнения (control flow), а поток данных (data flow), сопровождаемый монадическими Эффектами (о них чуть позже). Программа на чистом ФЯП – это такая многослойная матрешка, которая не делает ничего, пока не начнешь ее раскрывать слой за слоем. В процессе чего из нее иногда будут «вываливаться» Эффекты, сигнализирующие, что что-то там фактически программой было сделано.


Кроме того, декларативный подход «сверху-вниз» позволяет создавать библиотеки невероятной мощности: библиотека выкапывания всего и везде, библиотека варки любых блюд и т.п., которые дают сразу наборы функций из «верхней части» алгоритма (типа «варить до готовности»). Остаётся только дописать «нижнюю» половину – в каком порядке складывать в кастрюлю. Библиотека, работающая с монадами, работает с ЛЮБЫМИ монадами (которые удовлетворяют специальным «законам», общим для всех монад). Это обобщенное программирование, возведенное в Абсолют.


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


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


Примерами декларативных систем и их исполнительных механизмов являются:


Декларативный язык Императивный исполнитель
Почтовая адресация Почтальон, навигатор
HTML+CSS Движок рендера в браузере
ФЯП Компилятор языка, генерирующий императивный код для CPU
Excel Spreadsheet Движок вычислений Excel
SQL СУБД
ReactJS.Component ReactJS Library

Прошу к столу


Теперь наконец попробуем разобраться в терминах Теории Категорий на понятных условному сишнику/джависту примерах.


  • Категория – любой примитивный или составной тип данных: строка, число, пара строка-число (кортеж), массив чисел, тип функций (например, функция IntToStr имеет тип Integer -> String). Функциональные типы (т.е. сигнатуры) – полноценные типы. Можно из них тоже собрать кортеж или сложить в массив. Параметры обобщенных типов (те, которые с дженериками, т.е. в Array[Int], например, Array – это обобщённый тип, а Int – это его параметр) еще могут быть Ковариантными/Контравариантными/Инвариантными. Эта тема стоит отдельной статьи.


    • Важное уточнение: данное тут определение очень вольное. Категория — понятие еще более абстрактное, чем тип. Но для начала понимания, давайте примем пока это упрощение.

  • Морфизм – это любая функция, преобразующая один тип в другой. Вот IntToStr – это вполне себе морфизм. Итого: видим «морфизм» — читаем «функция конвертации». «Эндоморфизм» — это морфизм внутри категории, т.е. преобразование типа в самого себя. Функция «синус» вполне себе Эндоморфизм из Категории Double в нее же, хотя и крайне примитивный. Более сложный пример морфизма: преобразователь пары строк (username, password) в объект сессии.


  • «Ее Величество» Монада – это простой и банальный контейнер. Ее основная цель – обрабатывать данные в контейнере, не вынимая их наружу. Для этого к ней прицепили парочку функций (map). Например, если у нас есть монада-список (массив) чисел, то преобразование их в строки можно сделать прямо в массиве, сразу получив на выходе готовый массив строк, не заморачиваясь с циклами, созданием новых массивов и т.п.


    • Важное уточнение: когда я говорю «превратили числа в строки, не доставая из контейнера», я не имею в виду, что поменялось содержимое самого массива. Исходный массив (экземпляр) остается неизменным, но вызвав преобразование, мы получим второй массив (или дерево, или любой другой контейнер) идентичной структуры, только уже содержащий строки.
    • Но это только половина правды. Вторая половина состоит в том, что когда вы получили указатель на массив строк, никакого массива еще нет. Все вычисления "ленивые". Это означает, что пока вы не попытаетесь прочитать что-то из этого "массива" (который на самом деле просто аналог сишного Handle) ничего выполнено и сконвертировано не будет. Поэтому вы можете строить цепочки конверсий, которые мгновенно возвращают управление (потому что ничего не делают), и в конце, когда вам понадобится что-то достать из конечного контейнера, только тогда вся цепочка и раскрутится в последовательность вызовов конкретных IntToStr и им подобным.
    • В хаскеле функция такой обработки называется bind (>>=), что имеет корни в Теории Категорий. Ведь bind – это «связывание», т.е. функция bind фактически создает ребро в графе категорий (связывает узлы). В большинстве языков «здорового человека» эта функция называется map() (строго говоря, flatMap) («отобразить», «поставить в соответствие»). По мне, логичнее было бы ее назвать cast() («снять слепок», «преобразовать»), но меня почему-то не спросили.
    • Есть распространённая монада Option/Maybe, смысл которой в том, чтобы хранить одно единственное значение. Или не хранить. Например, мы могли бы сделать функцию StrToIntOption, которая бы принимала строку и возвращала Option[Int], т.е. такую монаду (контейнер), в которой либо лежало бы число (если строка в него парсится), либо не содержало бы ничего. С таким контейнером мы можем делать разные вещи, даже не проверяя, что в нем лежит. Например, можем умножить его содержимое на «2», взять синус, вывести на экран или отправить по сети. Для этого мы используем наш метод map(), передав в него функцию, которая должна сделать что-то полезное. Но фактически выполнена эта функция будет только, если в контейнере значение правда лежит (число распарсилось). Если в контейнере ничего нет, то ничего и не произойдет, ничего не умножится, ничего не отправится.
    • А вообще полезных монад люди придумали множество. Но все они несут один простой смысл, который описан выше. В любой большой системе можно наковырять с десяток служебных типов, которые можно было бы заменить монадическим типом. Монада-контейнер может накапливать в себе любой контекст, произошедшие ошибки, логирование и что угодно еще, не останавливая поток обработки и не засоряя код ненужным бойлер-плейтом. С помощью монад довольно элегантно решается большинство задач Аспектно-ориентированного программирования.
    • Мощь и удобство функции map() оказались настолько велики, что ее добавили к себе многие современные языки, далекие от чистого ФП.

  • Функтор – обработчик данных в контейнере-монаде. Функтор без монады – деньги на ветер.


    • Важное пояснение: в большинстве статей про ФП вам расскажут про функтор до монады, а потом, давая определение монады расскажут, что она тоже является функтором. После этого в голове обычно вообще все запутывается. Поэтому давайте договоримся, что вы пока забудете то, что сейчас прочитали в этом абзаце и просто продолжите читать.
    • Функтор — это та самая упомянутая выше функция map/bind.
      Осторожно!
      В этом месте в аудиторию врывается функциональный программист и орет, потрясая какой-то толстой книгой, что такое определение абсолютно некорректно. Ну а мы продолжаем...
      Смысл названия «злобный Функтор»: «функция над функциями», но он не отражает ее сути. Суть же – взять какой-нибудь морфизм (т.е. преобразователь типов) и применить его прямо внутри монады (контейнера). Т.е. Функтор – это Морфизм (преобразование) в Монаде (контейнере). Функтор выглядит со стороны это как будто вызвали функцию (`map`), передав в качестве параметра другую функцию (`IntToStr`), а в результате она вернет нам такой же массив, только уже со строками вместо чисел.
    • Теперь вернемся к теме «монада является функтором». На практике это означает, что в классе монады есть метод map. Все. Но по смыслу монада – это контейнер, а функтор – это оператор преобразования содержимого. Взболтайте, не смешивайте!
    • У Функтора есть двоюродный брат – Аппликативный функтор.

  • Аппликативный функтор – те же яйца, только к нему добавили еще одну фичу, чтобы конвертацию можно было делать отложенно в некоторых условиях, когда хотят скомпоновать содержимое нескольких контейнеров, опять же ничего не извлекая. Например (Option[username], Option[password]) -> Option[(username, password)]). С функцией map() мы бы не смогли сделать такую пару, не извлекая самих значений (нам сначала бы пришлось получить логин и пароль, а потом сложить в новый контейнер их пару). Поэтому тут добавляется еще одна функция ap() (от apply), которая «лениво» преобразует данные (как делал ее брат — Функтор) только когда кто-то начнет читать результирующий контейнер. На практике она возвращает частично примененную функцию – это ту, которая…


  • Частично примененные функции, Каррирование. Объяснение на простом примере функции с двумя переменными: давайте подставим в нее первый параметр, а второй оставим пока неизвестным. По факту получим функцию с одним параметром. Вот и все, мы только что сделали «каррирование» функции из арности k=2 в k=1. На самом деле в Хаскеле, например, вообще нет понятия количества параметров у функции в том смысле, как это делается в си-подобных языках. Например, если функции измерения расстояния надо 3 координаты (имеет сигнатуру Double -> Double -> Double -> Double), то мы можем в выражениях использовать ее как с одним, так и с двумя или с тремя параметрами. Отличия будут в типах возвращаемых результатов. В случае, если мы передадим все координаты, то она вернет «Double», если передадим на одну координату меньше – она вернет Double -> Double, т.е. функцию от одного параметра Double, если мы передадим всего одну координату, то результат будет иметь вид Double -> Double -> Double (функция от двух параметров Double, возвращающая Double).


    • А если мы такую же логику применим к обобщенным типам (дженерикам), т.е. рассмотим некий тип F[T1, T2, T3], то окажется что у такого типа есть конструктор, дающий конкретные реализации обобщенного типа (например F[Int, Double, String]). У этого конструктора будет 3 аргумента: T1, T2, T3. Действовать с ними он будет ровно так же, как вышеописанная функция. Т.е. его тоже можно "каррировать", уменьшая количество параметров, передавая часть из них. Только вот в этом случае не говорят о арности, а говорят о разных "кайндах" (kind). Почему? Потому что гладиолус.

  • Лямбда выражения и Замыкания. Лямбда исчисление имеет к ФП такое отношение, как Теория Категорий, т.е. никакое. Просто люди, привнесшие эту концепцию в ФП, были прожжёнными математиками, и дали ей такое название. Для того чтобы понять суть «лямбд» и «замыканий» не нужна высшая математика. Лямбда-выражение – это просто анонимная функция. Когда у тебя есть язык, весь состоящий из функций, и когда функции можно передавать в качестве значений другим функциям, то не очень хочется для каждой такой функции придумывать имя. Особенно если эта функция состоит из одной строки и тройки-другой слов.


  • Эффект – это один из столпов ФП, наравне с монадой (и настолько же абстрактен, как она). Эффект – это императивная часть программы. Любой программе, написанной на чистом и няшном ФП, приходится взаимодействовать с внешним миром. Любое взаимодействие заставляется выйти из теплого мирка контейнеров-монад в грязный реальный императивный мир и что-то вывести на экран, что-то принять по сети, прочитать текущее время и т.п. Кроме того любое извлечение данных из контейнера – это Эффект (т.к. с извлечением может быть запущена отложенная реальная обработка данных). Чтобы вывести распарсенное число на экран, нам придется-таки узнать, а было ли оно вообще распарсено (извлечь содержимое Option/Maybe). Не удивительно, что функциональщики стараются держать Эффекты под контролем. Весь прикол функционального мира состоит в том, что Эффекты до самого последнего момента тоже остаются монадными (т.е. упакованными в свой контейнер эффектов). Если где-то в коде ФЯП написано, что надо что-то вывести в консоль, то оно (текст) будет упаковано в монаду и доставлено вверх по кол-стеку прямо в функцию main. Функция main возвращает именно такую супер-монаду IO (а не void как в «сях»), которая собрала в себя всю логику программы, и все эффекты ввода-вывода в консоль. Только внутренний boot-код, сгенерированный компилятором, запустит исполнение Эффекта (извлечение контейнера IO) – откроет ящик Пандоры, из которого выскочат все реальные строки, вычисленные тут же «на лету» цепочками различных преобразований.


    • Эффект – это, на самом деле, венец всего ФП, после понимания которого наступает долгожданный катарсис «я наконец-то понял!».


Что дальше


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


  • Писать чистые функции – функции, которые оперируют только теми данными, которые получили на входе, никак их не меняя и возвращая обработанный результат.
  • Не использовать глобальные переменные и другие хранилища состояния в процессе обработки – выполнять Эффекты только в самом конце работы логики.
  • Аккуратнее с ООП. Изменяемые Объекты – это глобальные переменные. Старайтесь по возможности использовать immutable структуры данных.
  • Если ваш ЯП уже содержит функции map() и различные вариации монад (Option, Try и т.п.) старайтесь использовать их по максимуму.
  • В следующий раз попробуйте вместо цикла for написать map/forEach/fold/reduce или использовать другой Функтор, подходящей сигнатуры. Нет подходящего? Напиши его!

Заключение


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


Апдейт по мотивам комментариев


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

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 713

    +1
    >Нахрена
    >Сложно
    Да ладно, правда что-ль? Ну вот серьезно, неужели упомянутые вами же статьи типа … монады в картинках не дали вам никакого практического понимания? Что вы книги по теории категорий не поняли (не сразу) — охотно верю (сам такой), но есть же разные тексты, и их много хороших. Или вы хотите чтоб все и сразу, и в одном тексте?

    Я это не ради критики, если что. Мне скорее мотив автора хочется понять. Ведь было уже много текстов, и таких тоже (на мой взгляд таких).
      +36
      Сложность вникания в новые концепции всегда именно на старте. Немного привыкнув к терминологии и подходам, дальше двигаться намного проще. «Монады в картинках» мне не ответили на самый основной вопрос: нахрена нужна монада, функтор и прочие. Все упражнения с засовыванием и высовыванием из нее числа «3» не привели к понимаю, как из этого серпентария собрать что-то полезное. Пока не произошло то самое озарение: «да это же просто ленивый контейнер, мать его»!
        0
        >то самое озарение: «да это же просто ленивый контейнер, мать его»!
        Ну, да. Хотя на мой взгляд это не все детали отражает. Тут еще важно, что такое контейнер… что он владеет информацией о своей структуре, а функции из map этого знания не дают, и главное что ей его иметь и не нужно. Только контейнер знает, что он дерево — или список.

        Это же вроде вполне естественно? Ведь у вас же было понимание и ленивости, и контейнеров?
        +6
        Да ладно, правда что-ль? Ну вот серьезно, неужели упомянутые вами же статьи типа … монады в картинках не дали вам никакого практического понимания?

        Лично мне — возможно какое-то и дали, но назвать это пониманием можно лишь с натяжкой. Точно так же, как прочтение про абстрактный паттерн фабрика позволяет понять, что «ну он что-то там создаёт», но не позволяет прочувствовать, где, как и когда его полноценно применять, особенно если сложность проекта выше, чем манипуляции с числами и вводом пользователя.

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

        Правда, не могу сказать, что всё так радужно, ведь ФП приносит новые проблемы. К примеру, иммутабельные структуры (поправьте, если не прав) требуют наличия линз или схожих механизмов для удобной работы с ними. В языке, где их изначально нет, получается интересная ситуация, когда вроде как иммутабельные структуры это хорошо и все согласны, но вот фреймворк сериализации с ними не работает, ОРМ — не работает, генератор фейковых данных для юнит и интеграционных тестов — не работают.
          0
          Пишу 20 лет на ФП и никаких проблем ФП мне не принес. И сериализация и ОРМ все отлично работает.
            +2
            Пишу 20 лет на ФП и никаких проблем ФП мне не принес. И сериализация и ОРМ все отлично работает.

            Вы пишете 20 лет на языке, в котором изначально не было полноценных иммьютабл структур данных и средств работы с ними, и у вас ни один из компонентов экосистемы не имеет с ними проблем? Искренне завидую, у меня всё не так просто. Не подскажете, что за язык?
              0
              Как это не было иммутабельных структур, всегда были. Erlang
                +1
                Как это не было иммутабельных структур

                Ну, так, что я про это и уточняю в своём посте:
                В языке, где их изначально нет, получается интересная ситуация
                  0
                  да нет никакой связи между эрлангом и этим зубодробительным ФП, о котором написано в статье
                    –1

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

                0

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


                Что ни говори, без линз работать с иммутабельностью — больно.

                  0
                  Вот вообще не понимаю в чем проблема
                    +1
                    data Foo = MkFoo Int String String String String String 
                    data Bar = MkBar Foo Int String Int String Int String String
                    
                    fixNthFoo :: [Bar] -> Int -> Int -> [Bar]
                    fixNthFoo xs position value = undefined

                    Как реализовать функцию fixNthFoo чтобы в iBar поменять у Foo значение на value? Ну то есть то, что мы на расте каком-нибудь могли бы написать


                    fn fix_nth_foo(bars: &mut Bar, position: usize, value: i32) {
                       bars[position].foo.int_value = value;
                    }
                      0
                      Не знаю как на хаскеле записать, но вам надо вернуть новую структуру у которой нужное поле будет value.
                        +2

                        Ну запишите на чем угодно. Немного проспойлерю с линзами будет вот так:


                        fixNthFoo :: [Bar] -> Int -> Int -> [Bar]
                        fixNthFoo xs position value = xs . ix position  . foo . intValue .= value

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

                          0

                          Я так понимаю, что-то вроде этого:


                          newBars = bars.With(
                            pos, bars[pos].With(
                              foo: bars[pos].foo.With(
                                int_value: newValue
                              )))
                          
                            0

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

                              0

                              Ну уж нет, эти With с линзами рядом не стояли. Обратите внимание как приходится дублировать префиксы: bars, bars[pos], bars[pos].foo...


                              К слову, в Хаскеле тоже With есть, притом с языковой поддержкой, но линзам существовать это не мешает.

                                0

                                Да, в правы. Но в принципе какое-то переиспользование кода такой подход даст.

                                  +1
                                  Вы оба правы, в некотором смысле. Линзы — это не реализация With, а их обобщение.

                                  Ну вот как пресловутая полугруппа — это обобщение операции + (причём если вы про понятие знаете, то у вас уже не будет вызывать отторжение тот факт, что этот оператор сильно по разному ведёт себя со строками и с целыми числами… и, внезапно, станет понятно — что именно «не так» с «плавучкой»).

                                  Людей нужно учить ФП идя от примеров к общем понятиям. А не спускаясь от теории категорий к реальному миру.
                                  +3
                                  Ну уж нет, эти With с линзами рядом не стояли.
                                  Стояли-стояли. Именно что рядом.

                                  Вот пока монады начинающим будут описываться не на разнице между a().b().c() и a()?.b()?.c(), а на языке теории категорий — до тех пор ФП и будет являться, с точки зрения «непосвящённого», такой особой религией, а не чем-то практически полезным.

                                  Ну нельзя учить дошкольника арифметике, стартуя с аксиом Пеоно!

                                  Человек сначала должен понять почему -1 * -1 = 1 — это удобно и естественно (на примерах типа: если вам «простили» долг в один рубль, то теперь это сэкономленный рубль можно отнести в магазин) — а уже потом можно и про аксиоматику рассказать.

                                  А часто и вообще можно и без аксиоматики…
                                    +1
                                    Вот пока монады начинающим будут описываться не на разнице между a().b().c() и a()?.b()?.c()

                                    https://philipnilsson.github.io/Badness10k/escaping-hell-with-monads/ пойдёт?

                                      –1
                                      Нужно спрашивать у тех, кто не знает что такое монда до прочтения этой статьи… но выглядит неплохо.
                                        0

                                        На языке теории категорий и типов непонятно...

                                        0
                                        Я чего-то важного не понял, но
                                        do
                                        a <- getData
                                        b <- getMoreData a
                                        c <- getMoreData b
                                        d <- getEvenMoreData a c
                                        print d

                                        Приводится как универсальный ответ на все кейсы.
                                          +1
                                          Вы не поняли того, что в этом и есть суть монад: они нужны, чтобы разные задачи записать одинаково.

                                          Разумеется не все в мире задачи записываются в виде монад… но те что записываются — выглядят вот именно так.
                                            0
                                            А разница под капотом от кого зависит?
                                              0
                                              От монады.

                                              Ну вот рассмотрите более простую вещь: аппликатив.

                                              Сложить 2 и 2 (и получить 4) — это через "+". Сложить «2» и «2» (и получить «22») — это тоже через "+".

                                              От чего зависит «разница под капотом»?
                                                0
                                                Неправильно спросил. Мы (руками же?) пишем разную обработку разных сущностей, почему хорошо, если оно выглядит одинаково? Лично мне удобнее, если «2» + «2» вызывает ошибку и требует вызова специализированной функции конкатенации строк.
                                                  +1

                                                  Потому что тогда вы можете писать универсальные функции.


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

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

                                                      На какой-то предыдущей. Если функция ожидает число, то у неё написано Int или Num a => a или что-то подобное. А там, может, по построению число получается, или валидация происходит, или кто-то мамой клянётся.

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

                                                          Это тогда получается, что сначала проект должен был быть написан на не-ФП-языке, а потом всё выкинули и переписали на ФП-языке. Такие случаи мне неизвестны.


                                                          А на что посмотреть — ну какой-нибудь там ghc, например, говорят, внутри хорошо устроен. Но я туда пока не залезал, а мои личные проекты все довольно мелкие.

                                                            0
                                                            Это тогда получается, что сначала проект должен был быть написан на не-ФП-языке, а потом всё выкинули и переписали на ФП-языке. Такие случаи мне неизвестны.
                                                            Всё гораздо хуже. Спуститесь на ступеньку вниз и подумайте над другим примером: переходе с ассемблера на языки структурного программирования (неважно даже: BCL, C или Pascal).

                                                            Даже если проект и переписывается с ассмеблера на C — это, всё равно, выглядит как написание нового проекта с нуля… и человеку не умеющему в структурное программирование бывает очень сложно объяснить «зачем».

                                                            Вот встроить в проект высокого уровня кусочек на ассемблере — обычно без проблем. А вот «поднять» проект на более высокий уровень — нужно переписывать почти всегда…
                                                              0

                                                              ФП же не выше или ниже ПП и ООП, а просто другая ветвь. И не знаю как со встраиванием ООП кода в функциональный, но элементы ФП в ПП и ООП код я вставляю уже лет 20.

                                                            0
                                                            Pandoc написан на haskell
                                                          +4

                                                          Ну вот вы работаете с интами, которые приходят по сети по текстовому протоколу (HTTP)? Вот как вы гарантируете, что строки которые вам приходят — это числа?


                                                          Просто в какой-то момент парсите строки где надо, но везде в программе у вас статически проверяются что там где ожидаются инты передаются инты.

                                                +1

                                                Это и есть высокоуровневая абстракция

                                        +2

                                        А как эту композицию With теперь передать куда-то? Прелесть линз в том, что линза — это обычная функция, и композиция линз — тоже обычная функция, и можно написать функцию, которая принимает линзу и объект и что-то там с ними потом делает. Или, например, отсортировать список по значению какого-то хитровложенного ключа через что-то вроде sortBy (comparing (^.field.subfield.subsubfield)).

                                          0

                                          Тю, так я и не говорил, что with — лучше. Я просто сказал, что без линз это, по сути, единственный способ

                                        –2
                                        Ты забыл сказать, что линзы это не какаято языковая магия. Я не знаю как линзы работают с такими типами данных, позиционными структурами, у меня например не работает.
                                        Couldn't match type ‘()’ with ‘Bar’
                                        но вот для records надо ведь написать кучу кода, который по сути создает новую структуру на основе имеющейся, либо если писать лень — воспользоваться $(makeLenses ''Foo)
                                      –2
                                      replacenth(L,Index,NewValue) -> 
                                      {L1,[_|L2]} = lists:split(Index-1,L),
                                       L1++[NewValue|L2].
                                      
                                      1> replacenth([1,2,3,4,5],3,foo).
                                      [1,2,foo,4,5]
                                      

                                      Вот пример со списком в Erlang.
                                        +1

                                        Это вы сделали bars[position] = foo. А вас просили заменить поле на третьем уровне вложенности.

                                          0

                                          Так у вас тут список целых чисел, а у нас речь про список объектов, причем нам нужно заменить поле одного из подобъектов. Можете полный пример привести? Причем конечно надо учесть, что у обьетов есть и другие поля, которые трогать не надо (У Foo ещё 5 стринговых полей каких-то, у Bar тоже разные другие есть)

                                            0
                                            Что вы понимаете под объектами? Тут скорее о рекорде можно говорить.
                                            Что со списками, что с tuple идея та же самая, конструируете новый элемент с нужными полями.
                                              +1

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


                                              Что со списками, что с tuple идея та же самая, конструируете новый элемент с нужными полями.

                                              И что, это не займет километр кода? Если не займет, то можно пример? Если займет, то это неюзабельно и является пресловутыми проблемами ФП без линз

                                                0
                                                Да дело даже не в этом, займет или нет. У вас же просто может не быть нужного конструктора?
                                                  0

                                                  Конструктор всегда есть, иначе значение нельзя было бы создавать. Это не ООП где у структуры 10 конструкторов с разными уровнями видимости. В ФП модельки всегда анимичные, а всё поведение задается функциями

                                                  0
                                                  Если займет, то это неюзабельно и является пресловутыми проблемами ФП без линз

                                                  Тут важно, что значит «без линз»? В том же Elang «нарисовать» линзу (как пару fget/fset) — вообще не проблема… вот только пользоваться ей не сильно удобней (в смысле синтаксиса). «Объектов»-то нет… все честно :-) И record'ы, в этом смысле, это просто «сахарок» над кортежами…

                                                  В том смысле, что в случае хоть какой-то вложенности цепочка «линзовых» get/set'ов настолько же «удобна», как и спец. синтаксис record'ов.

                                                  А для того чтобы иметь возможность писать более менее «по-человечьи» — что-то типа dot notation с «присваиванием» (его, кстати тоже нет… все по взрослому :-) ) на конце — уже нужен parse transform. А во что именно «разворачивать» pt — дело, внезапно, десятое… в спец. синтаксис даже проще :-)

                                                  Вот и получается, что lens-фреймворков в Elang «очень даже есть», но, на практике, пользуют — если уж прям так «ломает» от спец. синтаксиса — pt либы, а не их.
                                                    +1

                                                    Так сила линз как раз в том, что это просто функции и там не нужен отдельный синтаксис

                                                      +1
                                                      Так сила линз как раз в том, что это просто функции и там не нужен отдельный синтаксис

                                                      Ф-ции для работы с кортежами (element/setelement) в Erlang и так есть. И от того, что ты сделаешь какой-нибудь
                                                      -record(lens, {get, set}).
                                                      
                                                      make_lens(N) ->
                                                        #lens{get = fun(R) -> element(N, R) end,
                                                          set = fun(V, R) -> setelement(N, R, V) end}.
                                                      

                                                      Пользоваться ими сильно удобнее не станет. И даже если где-нибудь «рядом» будет какой-нибудь
                                                      compose(Fs) ->
                                                        lists:foldl(fun(F, G) -> fun(X) -> F(G(X)) end, fun(V) -> V end, Fs).
                                                      

                                                      это мало что изменит в плане «удобства использования» :-)
                                                      +1
                                                      уже нужен parse transform

                                                      Так не нужен. Вон в хаскелле никакого parse transform и всё работает. Причем это не сахар к которому не подкопаться, а функции, которые можно дальше расширять/композировать/...

                                                        +1
                                                        Начинать надо с того, что в Haskell есть dot operator для композиции. Убери его, и все станет уже не так «красиво» :-)

                                                        А записывать «в столбик» и в Erlang никто не запрещает. Но оно… как там… «неюзабельно» практически. Что с линзами, что без.

                                                        Т.е. тут «рулят» не столько линзы — сами по себе, сколько синтаксис композиции.
                                                          +1

                                                          Использование любого другого оператора красоты не убавит.

                                                            +1
                                                            Само наличие отсутствия **оператора** для композиции — ещё как убавит.
                                                      –1
                                                      Можете сделать tuple_to_list() и дальше как в примере.
                                                      Вы пытаетесь повторить свой опыт из процедурных языков в ФП, сделать кальку. На практике такой необходимости нет. Мне еще ни разу не приходилось по индексу обращаться к полю записи.
                                                        +1

                                                        А кто тут говорит про обращение к полю записи по индексу?

                                                          0
                                                          а position это что?
                                                            +1

                                                            Индекс в массиве/списке/где-то ещё. Это не индекс поля записи.

                                                              0
                                                              Если выше почитаете, то речь идет о позиции в записи, а не индекс массива.
                                                                +1

                                                                Нет, не идёт.

                                                                  0
                                                                  Тогда не понятно чем пример со списком не устроил
                                                                    +1

                                                                    Тем, что он не полный.

                                                                      –2
                                                                      Он не полный потому-что вы хотите видеть не атомарное значение в списке, а структуру, но смысл от этого не меняется, вы не можете сделать деструктивное присваивание как в процедурных языках, но сконструировать новую структуру вам ни кто не запрещает. Я просто привел пример как это сделать, а будет там атом или рекорд значение не имеет.
                                                                        0

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


                                                                        Но вы не можете опровергнуть сложности с обновлением вложенных структур демонстрируя обновление простой структуры.

                                                                          0
                                                                          Фреймворки работают, но вот удачи обновить поле другого поля структуры, находящейся в каком-нибудь дереве или списке.

                                                                          Что ни говори, без линз работать с иммутабельностью — больно.

                                                                          Я не говорил что это просто, я говорил что не понимаю проблемы обновления иммутабельных структур. Все программы которые вы пишите — это про работу с данными, структурами какими-то сложными, если бы так просто было все, зачем тогда такие развесистые программы писать. Вы в процессе написания сложность стараетесь спрятать куда-то внутрь, упростить манипуляцию с данными.
                                                                            +2

                                                                            Ну вот и напишите как выглядит обновление вложенной иммутабельной структуры с тремя уровнями вложенности.


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

                                                                              –3
                                                                              Писать фигню, не проверив на работоспособность, я не хочу, а тратить время на решение проблемы, которой у меня ни когда не было нет времени. Я не говорю что это просто, я говорю что это возможно, это будет чуть больше кода чем для списка.
                                                                                +1
                                                                                Писать фигню, не проверив на работоспособность, я не хочу

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


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

                                                                    0
                                                                    а к полю тогда где обращение?
                                                                      +1

                                                                      Обращение к полю происходит по имени, а не по индексу. foo, int_value из примера на Rust — это как раз имена полей.

                                                                        0
                                                                        На всякий… в Erlang — «по честному» есть только кортежи… у полей которых, понятно, имен нет. Имена полей есть у т.н. record'ов. Но record'ы — это compile-time «сахар» над кортежами. Т.е. вот чтоб прям вообще «обобщить» через них уже не получится — всё равно всё сведется к mapping'у через какой-нибудь самописный
                                                                        index(FiledName, record_info(fields, RecordName)) + 1
                                                                        
                                                                        на element/setelement кортежей. Так что лучше про них не вспоминать.
                                                                          0
                                                                          На всякий… в Erlang — «по честному» есть только кортежи… у полей которых, понятно, имен нет. Имена полей есть у т.н. record'ов.

                                                                          А как же maps?
                                                                            0
                                                                            А как же maps?

                                                                            Совсем я старый стал :-( Спасибо, что поправили.
                                                                          –1
                                                                          Но как происходит обращение по имени в кортеже, у которого нет имен полей? так же сделать нельзя
                                                                          int_value нет у кортежа в расте, есть либо .0 .1 либо паттерг матчинг
                                                                            0

                                                                            А кто вам сказал, что там в Расте используются кортежи?

                                                                              0
                                                                              data Bar = MkBar Foo Int String Int String Int String String

                                                                              Ну когда показывают такую структуру в хаскеле и говорят об аналоге в расте, то я предполагаю что там аналогичная структура, а не совсем другая удобная.
                                                                              А как в хаскеле в такой структуре обратиться к полю по имени? или тут тоже имелся ввиду не кортеж, хотя описан кортеж? вобще знаю 1 способ
                                                                              foo :: Bar -> Foo
                                                                              foo (MkBar a _ _ _ _ _ _ _) = a
                                                                              но такую функцию надо предварительно написать
                                                                                +1

                                                                                Я так понимаю, типы тут записаны в таком виде просто для упрощения записи, думаю никто не будет возражать если заменить их на record. Проблема-то в другом.

                                                      –1
                                                      Ну он просто чуть по другому должен выглядеть :-) Чуть более «универсально»…

                                                      with(L, Predicate, Map) ->
                                                        fold(fun(Index, Elem, Acc) ->
                                                          case Predicate(Index, Elem) of
                                                            false -> [Elem|Acc];
                                                            true -> [Map(Elem)|Acc]
                                                          end
                                                        end, [], L).
                                                      


                                                      P.S. Позор на мои седины… стандартный fold же без индекса.

                                                      Но это поправимо:

                                                      fold(F, Acc, Index, [H|T]) ->
                                                          F(Index, H,  fold(F, Acc, Index + 1, T));
                                                      fold(F, Acc, _, []) -> Acc.
                                                      
                                                      fold(F, Acc, L) ->
                                                        fold(F, Acc, 1, L).
                                                      


                                                      Вроде так :-)
                                                        +1

                                                        Что-то в вашем примере я не вижу ни foo, ни bar. Да и вообще код делает настолько не то, что просили, что не даже отличий найти не получается.

                                                          0
                                                          with([1,2,3,4,5], fun (Idx, _) -> Idx == 3, fun(_) -> foo).
                                                          


                                                          Так понятней?
                                                            0

                                                            Это вы к первому примеру вернулись. А дальше-то что?

                                                              0
                                                              Это вы к первому примеру вернулись.

                                                              В смысле?! Реализация with — это больше ответ на:
                                                              Как реализовать функцию fixNthFoo чтобы в i-м Bar поменять у Foo значение на value? Ну то есть то, что мы на расте каком-нибудь могли бы написать


                                                              Оно — конечно — чуть более универсальнее получилось, чем… но, суть от этого не поменялась. Т.е. этот with делает — в том числе — и то, что replacenth. По сути — вся разница в том, что NewValue — это ф-ция от (чтобы можно было кортежами/рекордами рабоать)… ну и «индекс» задается через предикат… чтоб не только по числовому индексу можно было заменять.
                                                0
                                                Не хочется придираться к формулировкам в комментах, но то, что Object-Relational Mapping у вас отлично работает в Функциональной парадигме наводит на мысли, что не такая уж она и функциональная…
                                                  0
                                                  Согласен Erlang не такой уж и функциональный. Почему Object-Relational Mapping не должен работать?
                                                    0
                                                    Согласен Erlang не такой уж и функциональный.


                                                    ?! В каком смысле, если не секрет? Или это какая-то такая хитрая ирония?
                                                      0
                                                      Очень много споров по поводу функциональный он или нет. Высказывают мнение что чисто функциональный язык не пригоден для практического применения. Как по мне Erlang не является «чисто функциональным», но это функциональный язык. В Erlang есть глобальное состояние ETS,Mnesia. Сам Erlang не накладывает на вас каких-то обязательств по его использованию, не используйте ETS, пишите чистые функции.
                                                        0
                                                        ETS такое же глобальное состояние, как и самописное хранилище данных поверх gen_server или loop. То есть никакое не глобальное в принципе, потому что сам по себе язык не позволяет иметь глобальных состояний.

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

                                                        Глобальное состояние — это когда два и более потоков могут одновременно что-то записать в одну область памяти (переменную). Тут этого нет, потому что все операции поверх ETS синхронные, атомарные и изолированные (в силу того, что несмотря на всю свою асинхронность, Erlang строго синхронный язык)

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

                                                        Я не исключаю, что могут быть какие-то неизвестные мне особенности языка, которые позволяют стрельнуть другому потоку в ногу (кроме бинарников, про них в курсе)
                                                          0
                                                          Если вы работаете с ETS у вас функции получаются нечистые, вызов функции в разное время не гарантирует вам одинакового результата.
                                                          Erlang параллельный, когда работали на одном процессоре, была псевдопаралельность, планировщик распределял очередь заданий, а сейчас много ядер, много процессоров, и параллельность реальная.
                                                            0
                                                            Минутку, минутку. Разговор был о глобальном состоянии. Его нет.
                                                            Функции не чистые, тут я не спорю.

                                                            О параллельности. Тут все просто и изящно. Да, приложение, запущенное на двух и более процессорах, способно выполнять потоки Erlang параллельно. Но каждый поток при этом работает последовательно.

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

                                                            Поэтому даже в мультипроцессорной среде запросы в ETS остаются атомарными и изолированными, а ETS все больше превращается в bottleneck (с ростом количества запросов)
                                                              0
                                                              Есть еще persistent_term. ETS доступна все потомкам процесса его создавшего — это все равно проблема в рамках процесса. На уровне процессора поток конечно выполняется последовательно, процессор по другому не может. Поэтому я и говорю что используется псевдопаралельное выполнение, насколько я понимаю каждому процессу отводится свое время, потом передается другому и так по кругу, поправьте меня если я ошибаюсь.
                                                                0
                                                                Вот persistent_term интересная штука, я посмотрю. Не сталкивался особо раньше.

                                                                Да, в Erlang разделение, но не времени, а единиц выполнения. Вот тут неплохо вкратце описано: jlouisramblings.blogspot.com/2013/01/how-erlang-does-scheduling.html
                                                          –1
                                                          В Erlang есть глобальное состояние ETS,Mnesia.

                                                          Ни ETS, ни уже тем более Mnesia — которая лишь «фасад» для DETS, не являются «глобальным состоянием». Это отдельные процессы, со всеми вытекающими…

                                                          А «глобальное состояние» в Erlang действительно есть… это т.н. «словарь процесса». Но его использование — на практике — весьма ограничено. В первую очередь, безмерной радостью неизбежной «отладки в уме» :-) В то смысле, что — на практике — оно используется только если «действительно надо», выносится в отдельный процесс и «есть не просит».

                                                          Сам Erlang не накладывает на вас каких-то обязательств по его использованию, не используйте ETS, пишите чистые функции.
                                                          Ф-ция, использующая внешний — по отношению к вычисляющему её — процесс (будь то ETS/Mnesia, или какой-либо ещё), вполне себе чистая ф-ция. С чего вы взяли, что нет?!

                                                            0
                                                            Чистая функция должна быть детерминированой и независить от побочных эффектов. Если вы например хотите получить значение по ключу из ets, в функции, то значение может быть разным, два условия для чистоты не соблюдаются.
                                                              +1
                                                              Ф-ция, использующая внешний — по отношению к вычисляющему её — процесс (будь то ETS/Mnesia, или какой-либо ещё), вполне себе чистая ф-ция.

                                                              Нет, это уже совершенно точно не чистая функция. Любое обращение к внешнему процессу — побочный эффект, и, как следствие, никакой чистоты.
                                                          0
                                                          Не то, чтобы не должен, просто ORM это представление БД в объектной парадигме.
                                                            –1
                                                            В ООП для устранения семантического разрыва наверное ORM и делают, а для ФП устранять по сути нечего.
                                                              +2

                                                              Да ладно? Вы хотите сказать, что между ФП и SQL нет семантического разрыва? Это шутка такая?

                                                                0

                                                                без FRM никак? :)

                                                    +18

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


                                                    Например, из наиболее понравившегося:


                                                    Категория – любой примитивный или составной тип данных: строка, число, пара строка-число (кортеж), массив чисел, тип функций (например, функция IntToStr имеет тип Integer -> String).

                                                    Можно и так сказать, но это неконструктивно. Обычно рассматривают категорию типов (которая и называется Hask), а не категорию поверх одного типа (она очевидным образом имеет единственный объект). То есть, тогда уж лучше хотя бы сказать, что категория — множество всех типов языка.


                                                    «Эндоморфизм» — это морфизм внутри категории, т.е. преобразование типа в самого себя.

                                                    Любой морфизм заведомо лежит внутри категории, ну просто по определению категории и морфизма. Эндоморфизм — это морфизм из объекта категории в него же. Но до объектов категории, видимо, дело ещё не дошло.


                                                    У сишника/джависта, боюсь, после этих объяснений будет очень неправильное представление о том, что такое теоркат и что он изучает.


                                                    Функтор — это та самая упомянутая выше функция map/bind.

                                                    Только это две разных и совершенно неэквивалентных функции. map куда слабее, и функтор умеет map, но не умеет bind. И существуют типы, которые являются функторами (и даже аппликативными функторами), но не являются монадами — ZipList как пример.

                                                      +3
                                                      Обычно рассматривают категорию типов

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

                                                      У сишника/джависта, боюсь, после этих объяснений будет очень неправильное представление о том, что такое теоркат и что он изучает.

                                                      Дайте сишнику потрогать шарики и кубики, а потом задвигайте про обобщения.

                                                      Только это две разных и совершенно неэквивалентных функции. map куда слабее, и функтор умеет map, но не умеет bind. И существуют типы, которые являются функторами (и даже аппликативными функторами), но не являются монадами — ZipList как пример.

                                                      Еще раз: не надо объяснять, что бочка — это не цилиндр, а куб — это не ящик. Вы просто уже перебрались через эту пропасть непонимания. Я же строю мостик для тех, кто еще не по «ту сторону».
                                                        +3

                                                        Пожалуйста, объясняйте не как обычно, но "функтор" тогда в вашем объяснении это не тот "функтор", который используют все остальные люди. Ваш функтор между категорией Int в категорию Double это всего лишь функция, возможно от этого и остальное недопонимание того, чем что является.

                                                          0
                                                          Мне кажется вы невнимательно прочитали. Я не утверждал, что функтор это функция из Int в Double. Я как раз писал, что это функция работающая с функциями преобразования/отображения. Не совсем понял претензию.
                                                            +3

                                                            Ну вот вы пишете:


                                                            Функтор – обработчик данных в контейнере-монаде. Функтор без монады – деньги на ветер.

                                                            Какой обработчик данных в контейнере-монадке в эндофункторе в категории Int?


                                                            Или второй вопрос, из вашей фразы следует, что функтор обработчик ДЛЯ монады, в то время как между ними не отношение ВКЛЮЧАЕТ, а отношение ЯВЛЯЕТСЯ, то есть наследование, а не композиция. Определение запутывает и дает неправильное представление, в итоге.

                                                              –2
                                                              между ними не отношение ВКЛЮЧАЕТ, а отношение ЯВЛЯЕТСЯ, то есть наследование, а не композиция.

                                                              Да, верно. Но функтор в реальности — это одна функция map в классе. Поэтому я позволил себе вольность применить аналогии композиции, т.к. конечный результат одинаков в любом случае.

                                                              Какой обработчик данных в контейнере-монадке в эндофункторе в категории Int?

                                                              Соответственно, на этот вопрос можно ответить, если рассматривать функтор — как функцию map внутри монады (т.е. как ее поведение), а не как предок монады. Это не совсем корректно, признаю. Но для понимания на начальном этапе, мне кажется проще их отделить друг от друга. Я пока не придумал более корректной аналогии их отношения при сохранении интуитивности.
                                                                +5
                                                                Да, верно. Но функтор в реальности — это одна функция map в классе. Поэтому я позволил себе вольность применить аналогии композиции, т.к. конечный результат одинаков в любом случае.

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


                                                                Соответственно, на этот вопрос можно ответить, если рассматривать функтор — как функцию map внутри монады (т.е. как ее поведение), а не как предок монады. Это не совсем корректно

                                                                Совсем некорректно *

                                                          +4

                                                          Учить лучше так, чтобы потом не пришлось говорить «забудьте всё, чему вас учили в школе». А интуицию лучше создавать на частных примерах вроде той же категории множеств.

                                                            0

                                                            .

                                                            +1
                                                            У сишника/джависта, боюсь, после этих объяснений будет очень неправильное представление о том, что такое теоркат и что он изучает.
                                                            А и не пофиг ли? Задача ведь не «посвятить в тайну», а «научить этим пользоваться»!
                                                              +6

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

                                                                +4
                                                                Ну вот в этом вся и беда: все заявляют, что теоркат не нужен, а как только кто-то пытается объяснить ну хоть что-то «на пальцах» — его тут же «заваливают камнями» за «плохое общение с теоркатом»…
                                                                  +2

                                                                  Тут проблема не в объяснении на пальцах, а в том, что это объяснение создаёт максимум иллюзию понимания.

                                                                    –1
                                                                    Так вам шашечки или ехать? Вам нужны люди, способные написать код или способные поддржать дискуссию на должно уровне?

                                                                    Уверяю вас: очень малый процент разработчиков на C++ или, тем более, JavaScript способны описать на строгом математическом языке что они, всё-таки, делают.

                                                                    Тем не менее библиотеки они пишут и обновляют… а на Haskell — сплошной Жванецкий: «телевизор прекрасный, подпаяй там какой-то пустяк и отдыхай, смотри»…
                                                                      0

                                                                      Мне ехать, и именно поэтому я говорю, ну, в общем, то, что я говорю парой комментов выше.

                                                              0
                                                              del
                                                              +5

                                                              Знаете, я вот монады понимаю, даже трансформеры вроде осилил, но картинки вроде этой


                                                              image


                                                              Даже мне читаются с трудом

                                                                +2
                                                                Да, я согласен, картинки хороши лишь до определенного предела. А дальше делают только хуже.
                                                              +7

                                                              После прочтения ряда статей:



                                                              Категория – любой примитивный или составной тип данных...

                                                              Я, как очень плохо учивший математику, понял категорию как некое обобщение над множествами, а сами множества являются одной из категорий (Set). А тип, вроде как, как раз укладывается в теорию множеств, как множество всех его допустимых значений. Так тип категория или нет?


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


                                                              Функтор — отображение морфизма из одной категории в другую. Кстати, о map. После некоторых размышлений мне стало казаться, что название функции означает не то, что она применяет некую функцию к контейнеру (как мы привыкли во всех языках, где есть map), а именно отображение функции в другую категорию. Например, отображает морфизм "+" в категории чисел, в категорию "Maybe чисел". Мне это (либо очень очевидное, либо очень неправильное) понимание пришло в голову, если рассмотреть сигнатуру не как привычную функцию с двумя аргументами map(f, container) -> container, а как каррированную:


                                                              fmap:: (a->b) -> fa -> fb

                                                              Я прав?


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


                                                              Монада. Такая штука, которая, в отличие от функтора, умеет работать не только с функциями a -> b, но и a -> m b:


                                                              bind :: m a -> (a -> m b) -> m b

                                                              Вроде бы, понятно, зачем оно нужно. И понятно, какую проблему (по сравнению с функтором) решает эта сигнатура, на примере тех же Maybe очень понятно. Вот мы имеем цепочку каких-то функций, каждая возвращает Maybe, их друг с другом биндим, все очень круто. А вот почему монада — контейнер? То, что условный Maybe/Option/Nullable итп контейнеры — понятно. Зачем bind при работе с контейнерами — понятно. Но что-то интуитивного понимания нет. И еще почему "монада позволяет описать последовательность" (или что-то в этом роде)?


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

                                                                +6
                                                                После прочтения этой статьи я уже нихера не понимаю.

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


                                                                Так тип категория или нет?

                                                                Нет. То, что вы написали до того, верно.


                                                                Мне это (либо очень очевидное, либо очень неправильное) понимание

                                                                Это очевидное и правильное понимание. Вы правильно поняли смысл функторов. Вы лифтите функцию int -> string, чтобы она работала с Maybe int -> Maybe string. Вы можете записать fmap как fmap (a -> b) -> (f a -> f b).


                                                                Аппликативный функтор. А вот тут непонятно.

                                                                Обычный функтор берет чистую функцию и преобразовывает ее так, чтобы она принимала обернутые значения.


                                                                Аппликативный функтор берет обернутую функцию и преобразовывает ее так, чтобы она принимала обернутые значения. Т.е. аппликативный функтор лифтит функции, не разворачивая их.


                                                                А вот почему монада — контейнер?

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


                                                                Но с таким же успехом и функтор — контейнер, нет?

                                                                Если рассматривать неверные аналогии, то функтор куда ближе к контейнеру, чем монада.

                                                                  +1

                                                                  Благодарю за разъяснения

                                                                    0
                                                                    А она и не контейнер
                                                                    Тоже об "контейнер споткнулся". Если уж так хочется обобщений, то конвейер, а не контейнер.

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


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

                                                                    0
                                                                    Я, как очень плохо учивший математику, понял категорию как некое обобщение над множествами, а сами множества являются одной из категорий (Set). А тип, вроде как, как раз укладывается в теорию множеств, как множество всех его допустимых значений. Так тип категория или нет?

                                                                    Категория — это класс (про себя неформально можно считать "множеством") объектов, между которыми есть морфизмы. Строго говоря, сами объекты можно заменить на id-стрелки, тогда получится, что категория состоит только из морфизмов. Пример категории — Set, объекты категории — разные множества, морфизмы — отображения между этими множествами.


                                                                    Другой пример — категория типов с плавающей точкой, объекты — {Float, Double}, морфизмы — все возможные функции вида Float -> Double и Double -> Float между ними.


                                                                    Мне это (либо очень очевидное, либо очень неправильное) понимание пришло в голову, если рассмотреть сигнатуру не как привычную функцию с двумя аргументами map(f, container) -> container, а как каррированную:

                                                                    Ну так и есть, можно посмотреть на картинки бартоша, они так и рисуют:


                                                                    image
                                                                    Недаром слово "функтор" похоже на слово "функция".


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

                                                                    Аппликативный функтор — это тот же обычный функтор (правда, закрытый), но для которого опрделена натуральная трансформация из функции a -> b в функцию (f a -> f b). Формально можно почитать здесь.


                                                                    То есть с точки зрения теории, аппликативный функтор ничем не выделяется, это всё тот же функтор, но для которого заданы небольшие дополнительные ограничения. Но вот с точки зрения программирования эта разница очень большая, потому что если у вас есть два функтора то вы без LiftA2 их вместе никак не сцепите. А вам часто нужно из (A?, B?) получить (A, B)? или из двух списков получить множество их комбинаций, ну и прочие подобные вещи.


                                                                    Классическую вещь, которую можно сделать на аппликативах (но нельзя на обычных функторах) — контекстно-независимый парсер (контекстно-зависимый требует уже монаду).


                                                                    Вроде бы, понятно, зачем оно нужно. И понятно, какую проблему (по сравнению с функтором) решает эта сигнатура, на примере тех же Maybe очень понятно. Вот мы имеем цепочку каких-то функций, каждая возвращает Maybe, их друг с другом биндим, все очень круто. А вот почему монада — контейнер? То, что условный Maybe/Option/Nullable итп контейнеры — понятно. Зачем bind при работе с контейнерами — понятно. Но что-то интуитивного понимания нет. И еще почему "монада позволяет описать последовательность" (или что-то в этом роде)?

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


                                                                    img


                                                                    То есть я могу тут долго расписывать, но особого понимания это не даст. Куда лучше будет самому потыкать и получить представление, что это такое.


                                                                    Попробуйте объяснить человеку, что такое точка. "Ну, точка это… Точка!". Потому что все остальные геометрические фигуры — линии, квадраты, треугольники определяются как "множество точек, которые ..." (дальше подставить для каждой фигуры своё ограничение). А сама точка никак не определяется, она просто есть, и чтобы понимать геометрию нужно представлять, что это. Так и тут, слишком базовый блок, чтобы определения давали много смысла.

                                                                      0
                                                                      Категория — это класс (про себя неформально можно считать "множеством") объектов, между которыми есть морфизмы

                                                                      Ну нет, это вы малую категорию описали.


                                                                      Аппликативный функтор — это тот же обычный функтор (правда, закрытый), но для которого опрделена натуральная трансформация из функции a -> b в функцию (f a -> f b). Формально можно почитать здесь.

                                                                      Это вы обычный функтор расписали. А для аппликативного определена трансформация из f (a -> b) в (f a -> f b).

                                                                        0
                                                                        Ну нет, это вы малую категорию описали.

                                                                        Для целей объяснения малых достаточно, не так ли?


                                                                        Это вы обычный функтор расписали. А для аппликативного определена трансформация из f (a -> b) в (f a -> f b).

                                                                        очепятка, благодарю. Жалко, что время на редактирование вышло

                                                                          +1
                                                                          Для целей объяснения малых достаточно, не так ли?

                                                                          Да. По крайней мере, для целей программирования про существование немалых категорий можно вообще забыть.

                                                                      –2
                                                                      Про категории и функторы вы все поняли правильно. есть несколько эквивалентных определений. Можно смотреть на них с разных сторон и видеть разные следствия.

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

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

                                                                      Напомню, зачем нужен аппликативный функтор: обычный функтор не позволяет нам гарантировать отложенного исполнения эффектор (извлечения данных) при всех видах компоновки.
                                                                        –4
                                                                        З.Ы. Монада — тайпкласс.

                                                                        Тайпкласс — это способ предоставить монадическое поведение для типа, который такого поведения не имеет. Если ваш тип изначально реализует «интерфейс» монады, то тайпкласс не нужен.

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

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

                                                                        Окей. Но с таким же успехом и функтор — контейнер, нет?

                                                                        Да, он тоже способен работать как контейнер, но в практической работе менее удобен.
                                                                          +1
                                                                          Тайпкласс — это способ предоставить монадическое поведение для типа, который такого поведения не имеет. Если ваш тип изначально реализует «интерфейс» монады, то тайпкласс не нужен.

                                                                          Нет, тайпкласс — это не способ предоставить поведение. Это способ потребовать определенного поведения.

                                                                            –1
                                                                            Один требует — другой предоставляет. Розетка без вилки не имеет смысла. Требуют обычно интерфейсом, а предоставляют — его имплементацией. В случае тайпклассов имплементация отделена от дата-тайпа в инстанс.
                                                                              +1

                                                                              Угу, в розетку вставляется вилка. Но розетка вовсе не "предоставляет вилку для тех проводов, которые вилки не имеют", как вы написали в сообщении выше.

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

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


                                                                            Если ваш тип изначально реализует «интерфейс» монады, то тайпкласс не нужен.

                                                                            Щито? А как вы реализуете интерфейс без реализации тайпкласса?

                                                                              0
                                                                              Вас в каком ЯП интересует?
                                                                                0

                                                                                В хаскеле, например.

                                                                          +7

                                                                          Само по себе ФП не сложно… непривычно, может быть. Просто некоторые люди умеют сложно объяснять простые вещи.

                                                                            –3
                                                                            «Изменяемые Объекты – это глобальные переменные» — в этом и есть вся проблема современного ООП, где объект — это то, над чем можно производить действия (get/set, передавать его по ссылке и т.д.), поэтому их сейчас стремятся делать иммутабельными. Но «правильный» объект — это, то что должно само производить действия над другими участками кода в программе (а точнее переключать направления вычислений, подобно стрелочнику на ж/д путях) — а это уже ближе к реактивному программированию. Что-то пытался в этом смысле сделать Егор Бугаенко, но к сожалению кмк он чрезмерно обожествлял ООП.
                                                                              +2

                                                                              "Но «правильный» объект — это, то что должно само производить действия над другими участками кода"


                                                                              Фигня полная. Логику должен проверять перед запуском компилятор.

                                                                                0
                                                                                Компилятор должен проверять бизнес-логику?
                                                                                  +5

                                                                                  Конечно.

                                                                                +2

                                                                                Мне кажется, ООП в исполнении Егора по сути очень близко к ФП. Такая вот смычка парадигм. Я далеко не всегда с его логикой согласен, но мне кажется, он суть ООП в целом чувствует верно, и в идеале (в голове Алана Кея) действительно разница между ООП ФП куда меньше, чем думает средний программист.

                                                                                  0
                                                                                  >он суть ООП в целом чувствует верно
                                                                                  Ага, ага. Я помню предложение реализовать if в виде java объекта. Только знаете в чем проблема — что на тот момент в Java не было ленивости. А if — он по большому счету ленивый, потому что пока предикат не вычислили, ни then, ни else не вычисляются.

                                                                                  Поэтому сказать, что человек с такими воззрениями «верно чувствует в целом»… ну это такая, гм, натяжка.
                                                                                    0
                                                                                    Претензия ясна, но мне кажется, это вопросы второстепенные. Кстати, поправьте, но Standard ML тоже не ленивый язык, по крайней мере, в классической реализации, однако же функциональный, так что прямой связи тут нет.
                                                                                      0
                                                                                      > это вопросы второстепенные
                                                                                      Мне кажется, что непонимание отсутствия ленивости, и предложение вычислять then и else одновременно (как оно и было бы, если бы они были параметры метода) — ну оно показывает, что предложение не продумано. Прямой связи вообще — нет, а в частном случае вполне есть.
                                                                                        0
                                                                                        Я не совсем понимаю, как связано непонимание ленивости с упусканием «сути» ООП.
                                                                                    0
                                                                                    Я не уверен, что полностью согласен с Аланом Кейем (и про динамическую типизацию, и про то что «все есть объект»), но вот что yegor256 правильно делает — так это пытается облегчить отдельный объект, оставляя за ним только одну какую-то функцию. В результате у него получается цепочка из декораторов (ну вот просто Java такая), которая и есть по сути композицией функций из ФП.
                                                                                    И последний оставшийся шаг до правильного ООП — это разделить объекты на делающие и хранящие. Первые — это просто чистые функции ФП, вторые — это те самые мутабельные объекты, обменивающиеся сообщениями в духе Smalltalk.
                                                                                    В итоге имеем два типа акторов — объекты-состояния и функции.
                                                                                      0
                                                                                      Первые — это просто чистые функции ФП, вторые — это те самые мутабельные объекты, обменивающиеся сообщениями в духе Smalltalk.

                                                                                      Но что может сделать чистая функция с объектом, вся структура которого скрыта внутри, и он, по сути, имеет только какой-нибудь метод acceptMessage? Где-то должны жить ещё и значения, причём с API доступным для таких чистых функций. Да хоть те же количества, которые можно складывать между собой. Возможно, что я просто не до конца понимаю идею про обмен сообщениями.

                                                                                        0
                                                                                        Можно сделать таким образом: у «хранящего» объекта (а других и не должно быть) есть список переменных, доступных для чтения снаружи. Какие это переменные — определяется самим этим объектом. И пожалуйста, любой другой объект может взять эти данные и сделать что-то с ними для себя.

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

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

                                                                                        Можно на этом строить реактивное программирование, указав что в объекте его «входное поле» зависит от «выходного поля» другого объекта.

                                                                                        То есть здесь получается сообщения в духе обычных геттеров и сеттеров, но они не нарушают инкапсуляцию (в широком смысле) любого объекта.

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

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

                                                                                        upd
                                                                                          0

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

                                                                                            0
                                                                                            «Обычные методы в ООП по определению не чистые» — под «обычностью» я подразумевал то, откуда мы их берем. Но теперь мы должны сделать их стать чистыми (иначе зачем это все затевать).
                                                                                  +9

                                                                                  В статье столько ошибок, что даже и не знаешь, с чего начать.


                                                                                  Категории — это не типы. Это, если уж пытаться натянуть сову на глобус, системы типов. Или не типов. В категории Set объектами (базовыми элементами категорий) являются множества разных элементов, которые в каком-то смысле могут быть типами (множество всех целых чисел, множество всех строк и т.д.). В категории Hask (которая не совсем категория, но это вопрос практического свойства) базовыми элементами являются типы хаскеля. Конечно, можно создать категорию Double, где объектами будут литералы, а морфизмы будут задавать преобразования между этими литералами, но что это даст?


                                                                                  Эндоморфизмы — это не "тип в себя", это морфизм в пределах одной категории. Преобразование из string в int вполне себе эндоморфизм. Преобразование из Maybe в Maybe тоже эндоморфизм.

                                                                                  Монады — это не контейнеры. Как пример, есть монады IO и Reader не являются "контейнерами", в которые можно что-то положить. Монады — это абстракция вычислений в контексте. Если уж хотите использовать аналогии, то ближайшее к монаде, что можно придумать из "обычного" мира — это Promise. Вы связываете промисы в цепочки вычислений, и каждое вычисление находится в контексте того, что оно случится когда-то. И, как забавное следствие, вы не можете просто взять и избавиться от Promise, если уж вы начали его использовать.


                                                                                  На самом деле Promise не монада, но вполне могла бы ей быть.


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


                                                                                  • берет Promise<int>,
                                                                                  • неявно извлекает int,
                                                                                  • применяет к нему intToStr(x: int): string (чистая функция)
                                                                                  • оборачивает в Promise
                                                                                  • возвращает Promise<string>.

                                                                                  Если бы это была монада, то это выглядело бы так:


                                                                                  • берет Promise<int>,
                                                                                  • неявно извлекает int,
                                                                                  • применяет к нему intToStr(x: int): Promise<string> (монадическая функция, может делать внутри async/await)
                                                                                  • возвращает Promise<string>

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


                                                                                  Т.е., опять же, используя промисы как пример, у нас есть Promise<(x: int) -> string> и Promise<int>. Аппликативный функтор дает возможность применить Promise<(x: int) -> string> к Promise<int> и получить Promise<string>.


                                                                                  Что касается использования List как примера функторов и монад, и вытекающие отсюда неправильные представления о том, что это контейнеры, а монады и функторы работают с контейнерами. Нет. List не является массивом, List является абстракцией недетерминированных вычислений. То, что List можно вычислить, представив как массив, это лишь "ложный друг переводчика". Значение List[1,2,2,3] на самом деле описывает недетерминированное значение, которое с вероятностью 25% равно 1, с вероятностью 25% равно 3 и с вероятностью 50% равно 2.


                                                                                  Суммируя: вы ничего не поняли, но уже пошли объяснять остальным.

                                                                                    0

                                                                                    List это уж скорее похоже больше на итератор, чем на структуру данных.

                                                                                      +1

                                                                                      Опять же, нет. В ФП достаточно много перегруженных терминов.


                                                                                      List — это алгебраический тип данных. Какие-то упорядоченные данные в лениво вычисляющемся списке с произвольным доступом.


                                                                                      List — это функтор. Какие-то недетерминированно размазанные данные, реализованные поверх алгебраического типа данных List.


                                                                                      List — это монада. Цепочки вычислений над функтором List.

                                                                                      +1
                                                                                      Эндоморфизмы — это не "тип в себя", это морфизм в пределах одной категории.

                                                                                      Любой морфизм находится в пределах одной категории: категория определяется как класс (или множество, если это малая категория) объектов вместе с функцией Hom(a, b) — классом (или множеством, если это локально малая категория) морфизмов между объектами a и b, таким, что выполняются определённые условия.


                                                                                      Или в ту же, если это эндоморфизм.

                                                                                      Ввиду замечания выше не уверен, опечатка это или нет. Наверное, имелось в виду «если это эндофунктор».

                                                                                        0

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

                                                                                        +1
                                                                                        List не является массивом, List является абстракцией недетерминированных вычислений.

                                                                                        Я бы List считал абстракцией полного перебора. Для недетерминированных вычислений ему не хватает явных вероятностей.

                                                                                          +1

                                                                                          Лучше не брать промисы как пример, потому что для них не выполняются монадические законы, потому что then в ЖС это и map и bind одновременно (вот так вот). Поэтому a.then(b) может означать либо map, либо bind, поэтому написать на жс a.map(function_returning_promise) чтобы получить Promise<Promise<T>> не выйдет. А раз не выполняются законы, то всё плохо.

                                                                                            +5

                                                                                            О том, что теория без практики мертва, вещают тысячи практиков со всех сторон.
                                                                                            А вот этот пример с Promise — это отличный пример, когда практика без теории слепа.

                                                                                              –1
                                                                                              Это как раз хорошой пример — и именно потому что монадические законы не исполняются… хотя могли бы.

                                                                                              Потому там можно сразу показать — и что такое монада и почему работать с промисами неудобно… и, внезапно, неудобно с ними работать именно потому, что они — не монада…
                                                                                                +1

                                                                                                Я говорил про то, что промисы не надо брать как пример монады, потому что они не монады.


                                                                                                Чтобы показать, почему законы — полезны, они довольно хороши.

                                                                                                  +3
                                                                                                  Я говорил про то, что промисы не надо брать как пример монады, потому что они не монады.
                                                                                                  Это как раз неважно.

                                                                                                  Понимаете, у людей, которые «не понимают» монады основной вопрос — это не «что это», а «зачем это». Какие-то ящики на ножках, стрелочки, бог знает что… вот это вот всё… зачем?

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

                                                                                                  Вот вы же там показывали пример про липкую ленту.

                                                                                                  Где объясняется, что, типа «понять что это такое глядя на применения не удастся». Как раз удастся!

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

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

                                                                                                  Но для этого нужно, чтобы человек уже немного пообщался с коробками или хотя бы палками! А изобретение всё более красочных описаний этого предмета без того, чтобы показать — куда его можно применить… ничем хорошим не окончится.
                                                                                                    +1

                                                                                                    Ну я написал ту самую статью на которую линк был выше, лучше чем там объяснить и "что" и "почему" я наверное и не смогу.


                                                                                                    Про то, что и то и то надо объяснить — не спорю. Просто обратил внимание, вдруг кто не знал, что промис — не монадка. Кто-то, как видите, этого не знал.


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

                                                                                                    Ну то что практика даст понимание я не сомневаюсь. Вопрос, чтобы человек этой практикой занялся.

                                                                                                      +2
                                                                                                      Дык этим же все программисты уже занимаются!

                                                                                                      Вот все эти бесконечные промисы Option'ы, err, ok в Go и and_then в Rust… это всё попытки «закатить солнце вручную».

                                                                                                      Изобретение монад без введения понятия монада.

                                                                                                      И если человек осознаёт, что, условно говоря, он «всегда говорил прозой» (только корявой и неграмотной) — то понимание приходит быстрее, чем если он пытается прорваться через категории и картинки с коробками…
                                                                                                        0

                                                                                                        Вы написали эту статью?

                                                                                                          0

                                                                                                          Нет, статья написана за год до того как я начал ФП увлекаться. Но таких статей написано полно, про одно и то же, и все они — верны.

                                                                                                –1
                                                                                                В статье столько ошибок, что даже и не знаешь, с чего начать.

                                                                                                Наверное вы недостаточно внимательно прочитали вводную часть. То, что вы считаете «ошибками» — это умеренный отход от общности к конкретным примеры использования.
                                                                                                Моя цель дать частные примеры чтобы читающий сам понял общий принцип.

                                                                                                Категории — это не типы.

                                                                                                Категории — это не типы, а типы — категории. Категория — это очень широкое понятие.
                                                                                                Моя цель — см выше.

                                                                                                Это, если уж пытаться натянуть сову на глобус, системы типов. Или не типов. В категории Set объектами (базовыми элементами категорий) являются множества разных элементов, которые в каком-то смысле могут быть типами (множество всех целых чисел, множество всех строк и т.д.). В категории Hask (которая не совсем категория, но это вопрос практического свойства) базовыми элементами являются типы хаскеля. Конечно, можно создать категорию Double, где объектами будут литералы, а морфизмы будут задавать преобразования между этими литералами, но что это даст?

                                                                                                Это даст понятный новичку пример.

                                                                                                Эндоморфизмы — это не «тип в себя», это морфизм в пределах одной категории.

                                                                                                Это будет самопротиворечивым высказыванием, в категории «Тип Integer». ;)

                                                                                                Преобразование из string в int вполне себе эндоморфизм.

                                                                                                Вы забыли указать, что это верно только категории Hask. А категорий ух как много!

                                                                                                Монады — это не контейнеры.

                                                                                                Википедия с вами не согласна от слова «совсем». Монада — это функтор с дополнительной структурой. Эта структура позволяет инкапсулировать вычислительный контекст функтора. Без данной структуры нам бы не получилось локализировать контекст. Поэтому монада — это контейнер. Не отдельного значения, не массива, а контекста. Но в начале обучения это определение слишком абстрактно, а потому вредно, имхо.
                                                                                                +2

                                                                                                Пользуясь случаем, хочу прорекламировать одну очень хорошую книжку по теории категорий: F. Lawvere, Conceptual Mathematics: A First Introduction to Categories. Она очень понятно написана для тех, у кого есть базовое представление о теории множеств. Именна эта книга в своё время позволила мне перестать прятаться под диваном от одного вида коммутативных диаграмм и понять, наконец, что же означает "моноид в категории эндофункторов".


                                                                                                Проблема с теорией категорий в том, что если её сразу не к чему прицепить, то все понятия быстро выветрятся. В идеале, при вдумчивом ознакомлении с очередным понятием в голове должна выстраиваться связь в духе "Так это же [xxx]! Что ж сразу не сказали!" Конечно, лучше всего для этого подойдут познания в высшей алгебре и алгебраической же топологии. Но многие ими не обладают. Следующий по худшести вариант тоже почти очевиден: хорошее знание какого-нибудь ФП. Но если и этого нет, то, как ни странно, понимание ООП или реляционных баз данных тоже вполне сгодится, чтобы связать абстрактные понятия с чем-то очень знакомым. Книжка Ловера хороша тем, что там много примеров на базе категории множеств (Set), причём с иллюстрацией на конечных множествах, и многие из этих примеров можно почти сразу перевести на язык классов и объектов.

                                                                                                  0
                                                                                                  можем умножить его содержимое на «2»

                                                                                                  На «2» мы умножить не можем. Вот на 2 (без кавычек) — другое дело: все-таки проверка типов — она и с Option'ами делается (не знаю, как в хаскеле, но в скале точно).

                                                                                                    +2

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


                                                                                                    В статье много про монады, как будто в этом вся соль ФП. Но в реальности монады активно используются только в Хаскеле, 99% языков, которые мы бы назвали «функциональными» обходятся без них.


                                                                                                    Монады можно успешно заменить много чем. Например макросами и call/cc в scheme. Или async/await в C# или операцией [^] в smalltalk. Монада нужна теоретикам, что доказать некоторые свойства языка. А программисту до фонаря как это теоретик называет.


                                                                                                    Если мы хотим в типизированном языке сделать механизм вроде async/await нам нужен тип, в котором мы спрячем продолжение. Теоретики увидели сходство с математической структурой, именуемой монадой и понеслась. Но смысл остаётся все тем же — нужна штука, в которой будет жить continuation и механизм вызова этого continuation.

                                                                                                      +5
                                                                                                      Монады можно успешно заменить много чем. Например макросами и call/cc в scheme. Или async/await в C# или операцией [^] в smalltalk.

                                                                                                      Фрукты можно заменить много чем. Например сухофруктами или яблоками. Или апельсином красным или апельсиновыми дольками.

                                                                                                        0

                                                                                                        Я к тому, что семантически это практически одно и тоже, но без лишних математических ассоциаций.

                                                                                                          +4

                                                                                                          Очень странно слышать, что семантически монады IO, Async и Cont — это одно и тоже.
                                                                                                          Мой отсыл к фруктам был призван продемонстрировать логическую ошибку: отношение между монадами и, например, call/cc, — это отношение между общим и частным, и конечно же частное не заменяет общее.


                                                                                                          Если бы вы сказали, "монады — это абстракция над вычислениями в контексте, независимая от вычислителя", а потом добавили, что мол "на практике сама абстракция не нужна, а пользу приносит в-основном конкретная монада async/await и отлично работает", и с этим можно спорить (и я постарался бы этот тезис оспорить :-)), а в исходном виде фраза просто некорректна.