Я несколько раз начинал читать статьи из серии «Введение в функциональное программирование», «Введение в Теорию Категорий» и даже «Введение в Лямбда Исчисление». Причем и на русском, и на английском. Каждый раз впечатление было очень сходным: во-первых, много новых непонятных слов; во-вторых, много новых определений, которые возникают из ниоткуда; в-третьих, совершенно непонятно, как это использовать.
Самым непонятным и зубодробительным оказалось, наверное, Теория Категорий. Я освоился в ней только с третьего подхода. В первые два раза я честно все прочитал, кажется понял, но т.к. никакой связки с реальной жизнью она не имела, то спустя неделю она благополучно полностью выветривалась.
Попытки использовать как-то в работе изученные концепции разбивались о полное непонимание, как применить полученное глубокое знание. Ведь, напомню, что парадигму ФП (где-то удобнее, где-то не очень, но) можно использовать практически в любом ЯП, совсем необязательно для этого изучать условный Хаскель.
Кому эта статья
Эта статья для программистов, давно желавших понять Функциональное Программирование, пытавшихся что-то почитать на эту тему и упершихся в стену «да что, это блин за хрень такая, и зачем все так усложнять!?». Поэтому в этой статье я попытаюсь ответить на вопрос «зачем они это придумали», не сильно ударяясь в технические дебри. Я сегодня побуду таким «Робертом Киосаки от функционального программирования», который не столько учит вас финансовой функциональной грамотности, сколько мотивирует ее в себе развивать.
Дисклеймер
Я не претендую на звание эксперта в функциональном программировании или Теории Категорий. Далее в статье я излагаю довольно упрощенный и частный взгляд на довольно нетривиальные вещи. Прошу отнестись с пониманием к неточностям и ошибкам, ведь даже «истина, высказанная словами – есть ложь». Тем не менее я буду рад уточнениям и исправлениям, отправленным в личку.
Зачем нам Функциональное Программирование?
Изучение ФП делает разработчика профессиональнее. Я даже не буду приводить ссылки на пруфы, потому что в 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
или использовать другой Функтор, подходящей сигнатуры. Нет подходящего? Напиши его!
Заключение
Аппетит приходит во время еды. Постепенно развивая в себе функциональное чутье, со временем вы постепенно начнете «видеть» монады. Ваш код станет выразительнее, компактнее, надежнее. Но есть один недостаток: взглянув через год на свой код вам станет нестерпимо стыдно и захочется переписать его заново вдвое короче. По крайней мере так было у меня.
Апдейт по мотивам комментариев
Еще раз напомню, что данная статья не является академической. Данные тут определения — это вольные трактовки на конкретных житейских примерах, они способны вызывать батхёрт у тех, кто уже углубился в абстракции и топит за "чистоту". За строгостью формулировок я настоятельно рекомендую обратиться к авторитетным источникам.
sshikov
>Нахрена
>Сложно
Да ладно, правда что-ль? Ну вот серьезно, неужели упомянутые вами же статьи типа … монады в картинках не дали вам никакого практического понимания? Что вы книги по теории категорий не поняли (не сразу) — охотно верю (сам такой), но есть же разные тексты, и их много хороших. Или вы хотите чтоб все и сразу, и в одном тексте?
Я это не ради критики, если что. Мне скорее мотив автора хочется понять. Ведь было уже много текстов, и таких тоже (на мой взгляд таких).
barbalion Автор
Сложность вникания в новые концепции всегда именно на старте. Немного привыкнув к терминологии и подходам, дальше двигаться намного проще. «Монады в картинках» мне не ответили на самый основной вопрос: нахрена нужна монада, функтор и прочие. Все упражнения с засовыванием и высовыванием из нее числа «3» не привели к понимаю, как из этого серпентария собрать что-то полезное. Пока не произошло то самое озарение: «да это же просто ленивый контейнер, мать его»!
sshikov
>то самое озарение: «да это же просто ленивый контейнер, мать его»!
Ну, да. Хотя на мой взгляд это не все детали отражает. Тут еще важно, что такое контейнер… что он владеет информацией о своей структуре, а функции из map этого знания не дают, и главное что ей его иметь и не нужно. Только контейнер знает, что он дерево — или список.
Это же вроде вполне естественно? Ведь у вас же было понимание и ленивости, и контейнеров?
Guzergus
Лично мне — возможно какое-то и дали, но назвать это пониманием можно лишь с натяжкой. Точно так же, как прочтение про абстрактный паттерн фабрика позволяет понять, что «ну он что-то там создаёт», но не позволяет прочувствовать, где, как и когда его полноценно применять, особенно если сложность проекта выше, чем манипуляции с числами и вводом пользователя.
До сих пор не могу сказать, что понимаю монады с математической точки зрения, но как условный паттерн программирование начал их понимать исключительно после того, как сам написал несколько примитивных и на практике посмотрел на разницу кода с ними и без них. Возможно, это мне не хватает воображения и абстрактного мышления, но я всячески приветствую подобные статьи и очень надеюсь, что со временем ФП будет всё больше и больше проникать в индустрию.
Правда, не могу сказать, что всё так радужно, ведь ФП приносит новые проблемы. К примеру, иммутабельные структуры (поправьте, если не прав) требуют наличия линз или схожих механизмов для удобной работы с ними. В языке, где их изначально нет, получается интересная ситуация, когда вроде как иммутабельные структуры это хорошо и все согласны, но вот фреймворк сериализации с ними не работает, ОРМ — не работает, генератор фейковых данных для юнит и интеграционных тестов — не работают.
heiheshang
Пишу 20 лет на ФП и никаких проблем ФП мне не принес. И сериализация и ОРМ все отлично работает.
Guzergus
Вы пишете 20 лет на языке, в котором изначально не было полноценных иммьютабл структур данных и средств работы с ними, и у вас ни один из компонентов экосистемы не имеет с ними проблем? Искренне завидую, у меня всё не так просто. Не подскажете, что за язык?
heiheshang
Как это не было иммутабельных структур, всегда были. Erlang
Guzergus
Ну, так, что я про это и уточняю в своём посте:
erlyvideo
да нет никакой связи между эрлангом и этим зубодробительным ФП, о котором написано в статье
mwizard
в статье и про ФП ничего не написано. Написаны выдумки автора, использующие те же слова, но с каким-то безумным (и бездумно переусложненным) смыслом.
PsyHaSTe
Фреймворки работают, но вот удачи обновить поле другого поля структуры, находящейся в каком-нибудь дереве или списке.
Что ни говори, без линз работать с иммутабельностью — больно.
heiheshang
Вот вообще не понимаю в чем проблема
PsyHaSTe
Как реализовать функцию
fixNthFoo
чтобы вi
-мBar
поменять уFoo
значение на value? Ну то есть то, что мы на расте каком-нибудь могли бы написатьheiheshang
Не знаю как на хаскеле записать, но вам надо вернуть новую структуру у которой нужное поле будет value.
PsyHaSTe
Ну запишите на чем угодно. Немного проспойлерю с линзами будет вот так:
Покажите, как без линз на любом языке на ваш выбор на иммутабельных структурах это можно написать в одну строчку.
TheShock
Я так понимаю, что-то вроде этого:
PsyHaSTe
Да, линзы это и есть реализация этих
With
. Если у вас есть фреймворк который в общем случае позволяет такое записать, то это и будут линзы. Как вы сами при этом их называете — не важно.mayorovp
Ну уж нет, эти With с линзами рядом не стояли. Обратите внимание как приходится дублировать префиксы:
bars
,bars[pos]
,bars[pos].foo
...К слову, в Хаскеле тоже With есть, притом с языковой поддержкой, но линзам существовать это не мешает.
PsyHaSTe
Да, в правы. Но в принципе какое-то переиспользование кода такой подход даст.
khim
Вы оба правы, в некотором смысле. Линзы — это не реализация
With
, а их обобщение.Ну вот как пресловутая полугруппа — это обобщение операции
+
(причём если вы про понятие знаете, то у вас уже не будет вызывать отторжение тот факт, что этот оператор сильно по разному ведёт себя со строками и с целыми числами… и, внезапно, станет понятно — что именно «не так» с «плавучкой»).Людей нужно учить ФП идя от примеров к общем понятиям. А не спускаясь от теории категорий к реальному миру.
khim
Вот пока монады начинающим будут описываться не на разнице между
a().b().c()
иa()?.b()?.c()
, а на языке теории категорий — до тех пор ФП и будет являться, с точки зрения «непосвящённого», такой особой религией, а не чем-то практически полезным.Ну нельзя учить дошкольника арифметике, стартуя с аксиом Пеоно!
Человек сначала должен понять почему
-1 * -1 = 1
— это удобно и естественно (на примерах типа: если вам «простили» долг в один рубль, то теперь это сэкономленный рубль можно отнести в магазин) — а уже потом можно и про аксиоматику рассказать.А часто и вообще можно и без аксиоматики…
develop7
https://philipnilsson.github.io/Badness10k/escaping-hell-with-monads/ пойдёт?
khim
Нужно спрашивать у тех, кто не знает что такое монда до прочтения этой статьи… но выглядит неплохо.
VolCh
На языке теории категорий и типов непонятно...
Gryphon88
Я чего-то важного не понял, но
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d
Приводится как универсальный ответ на все кейсы.
khim
Вы не поняли того, что в этом и есть суть монад: они нужны, чтобы разные задачи записать одинаково.
Разумеется не все в мире задачи записываются в виде монад… но те что записываются — выглядят вот именно так.
Gryphon88
А разница под капотом от кого зависит?
khim
От монады.
Ну вот рассмотрите более простую вещь: аппликатив.
Сложить 2 и 2 (и получить 4) — это через "+". Сложить «2» и «2» (и получить «22») — это тоже через "+".
От чего зависит «разница под капотом»?
Gryphon88
Неправильно спросил. Мы (руками же?) пишем разную обработку разных сущностей, почему хорошо, если оно выглядит одинаково? Лично мне удобнее, если «2» + «2» вызывает ошибку и требует вызова специализированной функции конкатенации строк.
0xd34df00d
Потому что тогда вы можете писать универсальные функции.
Условный mconcat работает с произвольным моноидом, и вам не нужно писать отдельную функцию для сложения списка чисел, отдельную — для умножения списка чисел, отдельную — для конкатенации строк, и так далее.
Gryphon88
А валидируются входные данные на какой стадии? Просто мне кажется правильным поведением функции падение, если мы ей вместо числа строку подсунули.
0xd34df00d
На какой-то предыдущей. Если функция ожидает число, то у неё написано
Int
илиNum a => a
или что-то подобное. А там, может, по построению число получается, или валидация происходит, или кто-то мамой клянётся.Gryphon88
Посоветуете какой-нибудь опенсорсный проект, пожалуйста, чтобы я хоть понял, как ФП правильно готовить :) В идеале такой, чтобы ФП пришло с рефакторингом и сделало всё читаемей и менее многословным.
0xd34df00d
Это тогда получается, что сначала проект должен был быть написан на не-ФП-языке, а потом всё выкинули и переписали на ФП-языке. Такие случаи мне неизвестны.
А на что посмотреть — ну какой-нибудь там ghc, например, говорят, внутри хорошо устроен. Но я туда пока не залезал, а мои личные проекты все довольно мелкие.
khim
Даже если проект и переписывается с ассмеблера на C — это, всё равно, выглядит как написание нового проекта с нуля… и человеку не умеющему в структурное программирование бывает очень сложно объяснить «зачем».
Вот встроить в проект высокого уровня кусочек на ассемблере — обычно без проблем. А вот «поднять» проект на более высокий уровень — нужно переписывать почти всегда…
VolCh
ФП же не выше или ниже ПП и ООП, а просто другая ветвь. И не знаю как со встраиванием ООП кода в функциональный, но элементы ФП в ПП и ООП код я вставляю уже лет 20.
PsyHaSTe
Ну вот вы работаете с интами, которые приходят по сети по текстовому протоколу (HTTP)? Вот как вы гарантируете, что строки которые вам приходят — это числа?
Просто в какой-то момент парсите строки где надо, но везде в программе у вас статически проверяются что там где ожидаются инты передаются инты.
develop7
Это и есть высокоуровневая абстракция
0xd34df00d
А как эту композицию
With
теперь передать куда-то? Прелесть линз в том, что линза — это обычная функция, и композиция линз — тоже обычная функция, и можно написать функцию, которая принимает линзу и объект и что-то там с ними потом делает. Или, например, отсортировать список по значению какого-то хитровложенного ключа через что-то вродеsortBy (comparing (^.field.subfield.subsubfield))
.TheShock
Тю, так я и не говорил, что with — лучше. Я просто сказал, что без линз это, по сути, единственный способ
swelf
Ты забыл сказать, что линзы это не какаято языковая магия. Я не знаю как линзы работают с такими типами данных, позиционными структурами, у меня например не работает.
Couldn't match type ‘()’ with ‘Bar’
но вот для records надо ведь написать кучу кода, который по сути создает новую структуру на основе имеющейся, либо если писать лень — воспользоваться $(makeLenses ''Foo)
heiheshang
Вот пример со списком в Erlang.
mayorovp
Это вы сделали
bars[position] = foo
. А вас просили заменить поле на третьем уровне вложенности.PsyHaSTe
Так у вас тут список целых чисел, а у нас речь про список объектов, причем нам нужно заменить поле одного из подобъектов. Можете полный пример привести? Причем конечно надо учесть, что у обьетов есть и другие поля, которые трогать не надо (У Foo ещё 5 стринговых полей каких-то, у Bar тоже разные другие есть)
heiheshang
Что вы понимаете под объектами? Тут скорее о рекорде можно говорить.
Что со списками, что с tuple идея та же самая, конструируете новый элемент с нужными полями.
PsyHaSTe
Объект — структура с полями, поля можно доставать через проекции. Ну или просто HList фиксированного размера с проекциями на каждый элемент, если вам так больше нравится. Или рекорды, если нравится больше именно так. В общем, как ни говори, суть одна.
И что, это не займет километр кода? Если не займет, то можно пример? Если займет, то это неюзабельно и является пресловутыми проблемами ФП без линз
sshikov
Да дело даже не в этом, займет или нет. У вас же просто может не быть нужного конструктора?
PsyHaSTe
Конструктор всегда есть, иначе значение нельзя было бы создавать. Это не ООП где у структуры 10 конструкторов с разными уровнями видимости. В ФП модельки всегда анимичные, а всё поведение задается функциями
gBear
Тут важно, что значит «без линз»? В том же Elang «нарисовать» линзу (как пару fget/fset) — вообще не проблема… вот только пользоваться ей не сильно удобней (в смысле синтаксиса). «Объектов»-то нет… все честно :-) И record'ы, в этом смысле, это просто «сахарок» над кортежами…
В том смысле, что в случае хоть какой-то вложенности цепочка «линзовых» get/set'ов настолько же «удобна», как и спец. синтаксис record'ов.
А для того чтобы иметь возможность писать более менее «по-человечьи» — что-то типа dot notation с «присваиванием» (его, кстати тоже нет… все по взрослому :-) ) на конце — уже нужен parse transform. А во что именно «разворачивать» pt — дело, внезапно, десятое… в спец. синтаксис даже проще :-)
Вот и получается, что lens-фреймворков в Elang «очень даже есть», но, на практике, пользуют — если уж прям так «ломает» от спец. синтаксиса — pt либы, а не их.
AnthonyMikh
Так сила линз как раз в том, что это просто функции и там не нужен отдельный синтаксис
gBear
Ф-ции для работы с кортежами (element/setelement) в Erlang и так есть. И от того, что ты сделаешь какой-нибудь
Пользоваться ими сильно удобнее не станет. И даже если где-нибудь «рядом» будет какой-нибудь
это мало что изменит в плане «удобства использования» :-)
PsyHaSTe
Так не нужен. Вон в хаскелле никакого parse transform и всё работает. Причем это не сахар к которому не подкопаться, а функции, которые можно дальше расширять/композировать/...
gBear
Начинать надо с того, что в Haskell есть dot operator для композиции. Убери его, и все станет уже не так «красиво» :-)
А записывать «в столбик» и в Erlang никто не запрещает. Но оно… как там… «неюзабельно» практически. Что с линзами, что без.
Т.е. тут «рулят» не столько линзы — сами по себе, сколько синтаксис композиции.
mayorovp
Использование любого другого оператора красоты не убавит.
gBear
Само наличие отсутствия **оператора** для композиции — ещё как убавит.
heiheshang
Можете сделать tuple_to_list() и дальше как в примере.
Вы пытаетесь повторить свой опыт из процедурных языков в ФП, сделать кальку. На практике такой необходимости нет. Мне еще ни разу не приходилось по индексу обращаться к полю записи.
mayorovp
А кто тут говорит про обращение к полю записи по индексу?
heiheshang
а position это что?
mayorovp
Индекс в массиве/списке/где-то ещё. Это не индекс поля записи.
heiheshang
Если выше почитаете, то речь идет о позиции в записи, а не индекс массива.
mayorovp
Нет, не идёт.
heiheshang
Тогда не понятно чем пример со списком не устроил
mayorovp
Тем, что он не полный.
heiheshang
Он не полный потому-что вы хотите видеть не атомарное значение в списке, а структуру, но смысл от этого не меняется, вы не можете сделать деструктивное присваивание как в процедурных языках, но сконструировать новую структуру вам ни кто не запрещает. Я просто привел пример как это сделать, а будет там атом или рекорд значение не имеет.
mayorovp
Смысл тут ещё как меняется. Напоминаю, что вы ответили на комментарий о сложностях с обновлением вложенных структур данных. Никто не спорит же, что простые структуры данных и обновлять просто.
Но вы не можете опровергнуть сложности с обновлением вложенных структур демонстрируя обновление простой структуры.
heiheshang
Я не говорил что это просто, я говорил что не понимаю проблемы обновления иммутабельных структур. Все программы которые вы пишите — это про работу с данными, структурами какими-то сложными, если бы так просто было все, зачем тогда такие развесистые программы писать. Вы в процессе написания сложность стараетесь спрятать куда-то внутрь, упростить манипуляцию с данными.
mayorovp
Ну вот и напишите как выглядит обновление вложенной иммутабельной структуры с тремя уровнями вложенности.
Однако, то, сколько усилий вы прилагаете чтобы ни в коем случае не показывать никому этого кода, как раз и доказывает, что там всё далеко не так просто.
heiheshang
Писать фигню, не проверив на работоспособность, я не хочу, а тратить время на решение проблемы, которой у меня ни когда не было нет времени. Я не говорю что это просто, я говорю что это возможно, это будет чуть больше кода чем для списка.
PsyHaSTe
Ну это косвенно доказывает мою правоту, ведь в том чтобы написать однострочник на расте или хаскелле много времени не понадобилось, и его корректность тоже не вызывает сомнения хотя бы потому что кода мало и ошибиться в нём негде.
Зато можно ошибиться в 10-строчном сниппете, который вручную пытается восстановить record неизвестной структуры на третьем уровне вложенности.
swelf
а к полю тогда где обращение?
mayorovp
Обращение к полю происходит по имени, а не по индексу.
foo
,int_value
из примера на Rust — это как раз имена полей.gBear
На всякий… в Erlang — «по честному» есть только кортежи… у полей которых, понятно, имен нет. Имена полей есть у т.н. record'ов. Но record'ы — это compile-time «сахар» над кортежами. Т.е. вот чтоб прям вообще «обобщить» через них уже не получится — всё равно всё сведется к mapping'у через какой-нибудь самописный
на element/setelement кортежей. Так что лучше про них не вспоминать.Ogra
А как же maps?
gBear
Совсем я старый стал :-( Спасибо, что поправили.
swelf
Но как происходит обращение по имени в кортеже, у которого нет имен полей? так же сделать нельзя
int_value нет у кортежа в расте, есть либо .0 .1 либо паттерг матчинг
mayorovp
А кто вам сказал, что там в Расте используются кортежи?
swelf
Ну когда показывают такую структуру в хаскеле и говорят об аналоге в расте, то я предполагаю что там аналогичная структура, а не совсем другая удобная.
А как в хаскеле в такой структуре обратиться к полю по имени? или тут тоже имелся ввиду не кортеж, хотя описан кортеж? вобще знаю 1 способ
foo :: Bar -> Foo
foo (MkBar a _ _ _ _ _ _ _) = a
но такую функцию надо предварительно написать
mayorovp
Я так понимаю, типы тут записаны в таком виде просто для упрощения записи, думаю никто не будет возражать если заменить их на record. Проблема-то в другом.
gBear
Ну он просто чуть по другому должен выглядеть :-) Чуть более «универсально»…
P.S. Позор на мои седины… стандартный fold же без индекса.
Но это поправимо:
Вроде так :-)
mayorovp
Что-то в вашем примере я не вижу ни foo, ни bar. Да и вообще код делает настолько не то, что просили, что не даже отличий найти не получается.
gBear
Так понятней?
mayorovp
Это вы к первому примеру вернулись. А дальше-то что?
gBear
В смысле?! Реализация with — это больше ответ на:
Оно — конечно — чуть более универсальнее получилось, чем… но, суть от этого не поменялась. Т.е. этот with делает — в том числе — и то, что replacenth. По сути — вся разница в том, что NewValue — это ф-ция от (чтобы можно было кортежами/рекордами рабоать)… ну и «индекс» задается через предикат… чтоб не только по числовому индексу можно было заменять.
Busla
Не хочется придираться к формулировкам в комментах, но то, что Object-Relational Mapping у вас отлично работает в Функциональной парадигме наводит на мысли, что не такая уж она и функциональная…
heiheshang
Согласен Erlang не такой уж и функциональный. Почему Object-Relational Mapping не должен работать?
gBear
?! В каком смысле, если не секрет? Или это какая-то такая хитрая ирония?
heiheshang
Очень много споров по поводу функциональный он или нет. Высказывают мнение что чисто функциональный язык не пригоден для практического применения. Как по мне Erlang не является «чисто функциональным», но это функциональный язык. В Erlang есть глобальное состояние ETS,Mnesia. Сам Erlang не накладывает на вас каких-то обязательств по его использованию, не используйте ETS, пишите чистые функции.
eoffsock
ETS такое же глобальное состояние, как и самописное хранилище данных поверх gen_server или loop. То есть никакое не глобальное в принципе, потому что сам по себе язык не позволяет иметь глобальных состояний.
ETS реализует видимость глобального состояния и параллельного доступа, но на самом деле очередь запросов синхронная, а данные хранятся локально в памяти потока, порождающего ETS.
Глобальное состояние — это когда два и более потоков могут одновременно что-то записать в одну область памяти (переменную). Тут этого нет, потому что все операции поверх ETS синхронные, атомарные и изолированные (в силу того, что несмотря на всю свою асинхронность, Erlang строго синхронный язык)
Mnesia же такое же глобальное состояние, как и любая DBMS или обертка для хранения данных на диске. Формально да, для приложения Mnesia хранит данные глобально, но каждый процесс все равно получает свою копию данных, и все еще не может интерферировать с данными других процессов.
Я не исключаю, что могут быть какие-то неизвестные мне особенности языка, которые позволяют стрельнуть другому потоку в ногу (кроме бинарников, про них в курсе)
heiheshang
Если вы работаете с ETS у вас функции получаются нечистые, вызов функции в разное время не гарантирует вам одинакового результата.
Erlang параллельный, когда работали на одном процессоре, была псевдопаралельность, планировщик распределял очередь заданий, а сейчас много ядер, много процессоров, и параллельность реальная.
eoffsock
Минутку, минутку. Разговор был о глобальном состоянии. Его нет.
Функции не чистые, тут я не спорю.
О параллельности. Тут все просто и изящно. Да, приложение, запущенное на двух и более процессорах, способно выполнять потоки Erlang параллельно. Но каждый поток при этом работает последовательно.
Потоки взаимодействуют через посылку сообщений, и мейлбокс каждого процесса обрабатывается процессом последовательно, сообщение за сообщением.
Поэтому даже в мультипроцессорной среде запросы в ETS остаются атомарными и изолированными, а ETS все больше превращается в bottleneck (с ростом количества запросов)
heiheshang
Есть еще persistent_term. ETS доступна все потомкам процесса его создавшего — это все равно проблема в рамках процесса. На уровне процессора поток конечно выполняется последовательно, процессор по другому не может. Поэтому я и говорю что используется псевдопаралельное выполнение, насколько я понимаю каждому процессу отводится свое время, потом передается другому и так по кругу, поправьте меня если я ошибаюсь.
eoffsock
Вот persistent_term интересная штука, я посмотрю. Не сталкивался особо раньше.
Да, в Erlang разделение, но не времени, а единиц выполнения. Вот тут неплохо вкратце описано: jlouisramblings.blogspot.com/2013/01/how-erlang-does-scheduling.html
gBear
Ни ETS, ни уже тем более Mnesia — которая лишь «фасад» для DETS, не являются «глобальным состоянием». Это отдельные процессы, со всеми вытекающими…
А «глобальное состояние» в Erlang действительно есть… это т.н. «словарь процесса». Но его использование — на практике — весьма ограничено. В первую очередь, безмерной радостью неизбежной «отладки в уме» :-) В то смысле, что — на практике — оно используется только если «действительно надо», выносится в отдельный процесс и «есть не просит».
Ф-ция, использующая внешний — по отношению к вычисляющему её — процесс (будь то ETS/Mnesia, или какой-либо ещё), вполне себе чистая ф-ция. С чего вы взяли, что нет?!
heiheshang
Чистая функция должна быть детерминированой и независить от побочных эффектов. Если вы например хотите получить значение по ключу из ets, в функции, то значение может быть разным, два условия для чистоты не соблюдаются.
Ogra
Нет, это уже совершенно точно не чистая функция. Любое обращение к внешнему процессу — побочный эффект, и, как следствие, никакой чистоты.
Busla
Не то, чтобы не должен, просто ORM это представление БД в объектной парадигме.
heiheshang
В ООП для устранения семантического разрыва наверное ORM и делают, а для ФП устранять по сути нечего.
mayorovp
Да ладно? Вы хотите сказать, что между ФП и SQL нет семантического разрыва? Это шутка такая?
VolCh
без FRM никак? :)
0xd34df00d
По факту оказывается, что сложно. Автор сам не разобрался даже в базовой терминологии теорката, а уже пошёл писать свою статью по монадам. Всё прям как в мемесах.
Например, из наиболее понравившегося:
Можно и так сказать, но это неконструктивно. Обычно рассматривают категорию типов (которая и называется Hask), а не категорию поверх одного типа (она очевидным образом имеет единственный объект). То есть, тогда уж лучше хотя бы сказать, что категория — множество всех типов языка.
Любой морфизм заведомо лежит внутри категории, ну просто по определению категории и морфизма. Эндоморфизм — это морфизм из объекта категории в него же. Но до объектов категории, видимо, дело ещё не дошло.
У сишника/джависта, боюсь, после этих объяснений будет очень неправильное представление о том, что такое теоркат и что он изучает.
Только это две разных и совершенно неэквивалентных функции.
map
куда слабее, и функтор умеетmap
, но не умеетbind
. И существуют типы, которые являются функторами (и даже аппликативными функторами), но не являются монадами — ZipList как пример.barbalion Автор
Ключевое слово тут «обычно». Если бы я стремился написать статью «как обычно», то я бы ее не писал, а просто поставил ссылку на одну из сотни статей по теме. Моя цель была в том, чтобы дать привязку к понятным «сишнику» терминам и примерам. Категория типов — на одну ступеньку абстракции выше, чем категория-тип. Поэтому этот пример понять намного проще. А от него уже двинуться дальше по ступенькам абстракций.
Дайте сишнику потрогать шарики и кубики, а потом задвигайте про обобщения.
Еще раз: не надо объяснять, что бочка — это не цилиндр, а куб — это не ящик. Вы просто уже перебрались через эту пропасть непонимания. Я же строю мостик для тех, кто еще не по «ту сторону».
PsyHaSTe
Пожалуйста, объясняйте не как обычно, но "функтор" тогда в вашем объяснении это не тот "функтор", который используют все остальные люди. Ваш функтор между категорией Int в категорию Double это всего лишь функция, возможно от этого и остальное недопонимание того, чем что является.
barbalion Автор
Мне кажется вы невнимательно прочитали. Я не утверждал, что функтор это функция из Int в Double. Я как раз писал, что это функция работающая с функциями преобразования/отображения. Не совсем понял претензию.
PsyHaSTe
Ну вот вы пишете:
Какой обработчик данных в контейнере-монадке в эндофункторе в категории Int?
Или второй вопрос, из вашей фразы следует, что функтор обработчик ДЛЯ монады, в то время как между ними не отношение ВКЛЮЧАЕТ, а отношение ЯВЛЯЕТСЯ, то есть наследование, а не композиция. Определение запутывает и дает неправильное представление, в итоге.
barbalion Автор
Да, верно. Но функтор в реальности — это одна функция map в классе. Поэтому я позволил себе вольность применить аналогии композиции, т.к. конечный результат одинаков в любом случае.
Соответственно, на этот вопрос можно ответить, если рассматривать функтор — как функцию map внутри монады (т.е. как ее поведение), а не как предок монады. Это не совсем корректно, признаю. Но для понимания на начальном этапе, мне кажется проще их отделить друг от друга. Я пока не придумал более корректной аналогии их отношения при сохранении интуитивности.
PsyHaSTe
Функтор в реальности — это тайпкласс, и для типа его реализуют. В чем разница между функцией и интефрейсом (тайпклассом), в котором есть такая функция, думаю, объяснять не надо.
Совсем некорректно *
0xd34df00d
Учить лучше так, чтобы потом не пришлось говорить «забудьте всё, чему вас учили
в школе». А интуицию лучше создавать на частных примерах вроде той же категории множеств.khim
0xd34df00d
Пофиг, только на сам теоркат, а не на некорректность его объяснения. Серьёзно, для того, чтобы продуктивно писать на хаскеле, не надо знать ничего про теоркат, даже про функторы знать не надо, достаточно представлять, что это такой интерфейс со смешным названием, для которого выполняется полтора требования, и всё.
khim
Ну вот в этом вся и беда: все заявляют, что теоркат не нужен, а как только кто-то пытается объяснить ну хоть что-то «на пальцах» — его тут же «заваливают камнями» за «плохое общение с теоркатом»…
0xd34df00d
Тут проблема не в объяснении на пальцах, а в том, что это объяснение создаёт максимум иллюзию понимания.
khim
Так вам шашечки или ехать? Вам нужны люди, способные написать код или способные поддржать дискуссию на должно уровне?
Уверяю вас: очень малый процент разработчиков на C++ или, тем более, JavaScript способны описать на строгом математическом языке что они, всё-таки, делают.
Тем не менее библиотеки они пишут и обновляют… а на Haskell — сплошной Жванецкий: «телевизор прекрасный, подпаяй там какой-то пустяк и отдыхай, смотри»…
0xd34df00d
Мне ехать, и именно поэтому я говорю, ну, в общем, то, что я говорю парой комментов выше.
soniq
del
PsyHaSTe
Знаете, я вот монады понимаю, даже трансформеры вроде осилил, но картинки вроде этой
Даже мне читаются с трудом
sshikov
Да, я согласен, картинки хороши лишь до определенного предела. А дальше делают только хуже.