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

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

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

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

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

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

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

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

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

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


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

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;
}
Не знаю как на хаскеле записать, но вам надо вернуть новую структуру у которой нужное поле будет value.

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


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

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

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


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

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

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


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

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

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

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

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

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

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

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

А часто и вообще можно и без аксиоматики…
Нужно спрашивать у тех, кто не знает что такое монда до прочтения этой статьи… но выглядит неплохо.
Я чего-то важного не понял, но
do
a <- getData
b <- getMoreData a
c <- getMoreData b
d <- getEvenMoreData a c
print d

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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

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

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


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

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

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

Ты забыл сказать, что линзы это не какаято языковая магия. Я не знаю как линзы работают с такими типами данных, позиционными структурами, у меня например не работает.
Couldn't match type ‘()’ with ‘Bar’
но вот для records надо ведь написать кучу кода, который по сути создает новую структуру на основе имеющейся, либо если писать лень — воспользоваться $(makeLenses ''Foo)
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.

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

Ф-ции для работы с кортежами (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).

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

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

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

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

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

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


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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

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).


Вроде так :-)

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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


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

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


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


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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


image


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

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

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



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

Я, как очень плохо учивший математику, понял категорию как некое обобщение над множествами, а сами множества являются одной из категорий (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 при работе с контейнерами — понятно. Но что-то интуитивного понимания нет. И еще почему "монада позволяет описать последовательность" (или что-то в этом роде)?


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

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

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


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

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


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

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


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

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


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


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

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


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

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

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

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


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

Я, как очень плохо учивший математику, понял категорию как некое обобщение над множествами, а сами множества являются одной из категорий (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


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


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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

upd

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

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

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


Категории — это не типы. Это, если уж пытаться натянуть сову на глобус, системы типов. Или не типов. В категории 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.


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

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


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


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


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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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


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


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


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

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

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

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

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


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

Очень странно слышать, что семантически монады IO, Async и Cont — это одно и тоже.


Вы меня неправильно поняли или я неточно выразился. Основная функция монады это что-то вроде
Monad<T> MonadStepFunction(T value, Func<T> continuation)

— Maybe — не будет вызывать continuation если value == None
— List — работает только если value это список и будет вызывать continuation для каждого элемента списка
— Async — работает для чего-то вроде Promise и вызовет continuation когда Promise завершится.

Тут ключевое то, что у нас в распоряжении есть текущее значение и продолжение. call/cc тоже нам дает продолжение, а макросы позволяют сделать что-то вроде do нотации. Аналогично async/await + TaskBuilder + Awaitable дают нам продолжения в C#. Если напрячься, то можно на C# изобразить maybe. Выглядеть будет так


async Maybe<int> Func1() => 1; //наш  TaskBuilder построить Maybe.Just(1)
Maybe<int> Func2() => Maybe.Nothing; //а тут мы сами строим Maybe
int Func3i() => 2;

Func<Maybe<T>> LiftMaybe<T>(Func<T> f) =>  //можно трансформировать функции
    () => try { return Maybe.Just(f); } catch { return Maybe.Nothing; };

Maybe<int> Func3 = LiftMaybe(Func3i);

async Maybe<int> MyFunc() {
    var myMaybe1 = await Func1();
    var myMaybe2 = await Func2(); //тут закончится вычисление
    var myMaybe3 = await Func3();
//если бы сюда попали, то TaskBuilder построил бы нам 
//Maybe.Just(myMaybe1 + myMaybe2 + myMaybe3)
    return myMaybe1 + myMaybe2 + myMaybe3; 
}

Maybe<int> b = MyFunc(); //без await
if (b != Maybe<int>.Nothing) Console.WriteLine(b.Value); 


— «await Maybe» имеет тип T.
— любая функция, которая хочет делать «await Maybe» должна возвращать Maybe.

Можно извратиться еще больше и сделать что-то аналогичное с помощью LINQ. Опять же, все что нужно это продолжение.

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

С учетом того, что сказано выше я бы сказал «монады вместе с do-нотацией — это механизм, который позволяет получить и удобно работать с продолжениями, окружающая эта понятие математика не имеет значения». А потом бы я добавил — «на практике не важно как называется и чем обоснован механизм получения продолжения, значением имеет удобство синтаксиса. В C# удобно только для async/await, в scheme и smalltalk все шикарно».

Ну и давайте спорить :)
>> Если бы вы сказали, «монады — это абстракция над вычислениями в контексте, независимая от вычислителя», а потом добавили, что мол «на практике сама абстракция не нужна…

Извините мне вот стало „легко и понятно“, когда я для себя понял что зачем монада — это просто такой способ разделить вычисления над значениями и `контекст` доставшийся от предыдущих вычислений.
При этом: „монада это просто такой способ“ — тут слово просто обманчиво, поскольку в него запихали много чего, позволяющие получать интуитивно понятный результат при цепочках вычислений, и использовать IO как `контекст`(и наверняка вы добавите много чего ещё в это „просто“).

Так вот вопрос: интуитивно-удобное для меня понимание довольно близко к тому, с чем вы могли бы поспорить. Есть какая-то конкретная проблема в этом (удобном для меня) понимании?
Все нормальные языки уже давно большую часть ценных идей впитали.

Ради интереса, какие это идеи и какие языки? Я работал с C#, VB, Java и там не хватает очень многих вещей, которые есть даже в примитивнейших F#/Scala, не говоря уже про более развитые языки.
Дабы не быть голословным, несколько примеров из C# (часть этих вещей уже есть, но появились очень-очень недавно):
1. Non-nullable types by default (Optional, Maybe, etc) — огромное количество ошибок у нас возникает как раз по причине NPE. Наконец появилось в последнем C#, Java не смотрел.
2. Exhaustive pattern matching and discriminated unions — очень помогают для описания раздельных состояний. Пока не завезли.
3. Records (Scala's case classes) — в разы уменьшают количество бесполезного кода при описании структур. Также не завезли, есть 3P решения.
  1. Exhaustive pattern matching and discriminated unions — очень помогают для описания раздельных состояний. Пока не завезли.

Не могу не пропиарить свой пет-проект: https://github.com/Hirrolot/poica. Это тип-суммы с сопоставлением с образом (с проверкой исчерпаемости времени компиляции) на чистом Си. Можно теперь делать так:


typedef struct {
    const struct Expr *left, *right;
} ExprPair;

SUM(
    Expr,
    VARIANT(MkConst OF double)
    VARIANT(MkAdd OF ExprPair)
    VARIANT(MkSub OF ExprPair)
    VARIANT(MkMul OF ExprPair)
    VARIANT(MkDiv OF ExprPair)
);

double eval(const struct Expr *expr) {
    MATCH(expr) {
        CASE(MkConst, number) {
            return *number;
        }
        CASE(MkAdd, add) {
            return eval(add->left) + eval(add->right);
        }
        CASE(MkSub, sub) {
            return eval(sub->left) - eval(sub->right);
        }
        // ...
    }
}

До первого релиза ещё нужно попахать, т.к. планируются алг. эффекты, но именно функционал тип-сумм уже использовать можно :)

  1. всё ещё отстой, потому что функцию T? MaybeSomething<T>() написать не получится. компилятор потребует навесить констрейнт struct/class. Поэтому у меня в коде лапша подобной прелести:

Скрытый текст
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) 
where T : class =>
    source.Where(x => x is {})!;

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) 
where T : struct =>
    source.Where(x => x is {}).Select(x => x.GetValueOrDefault());

public static T? FirstOrNull<T>(this IEnumerable<T> source) 
where T : class =>
    source.FirstOrDefault();

public static T? FirstOrNull<T>(this IEnumerable<T?> source) 
where T : struct =>
    source.FirstOrDefault();

public static T? FirstOrNull<T>(this IEnumerable<T> source, object? _ = null) 
where T : struct =>
    source.Cast<T?>().FirstOrDefault();

... тысячи их

  1. Очень больно без Either, очень часто нужны, пока делаем на эксепшнах и надеемся, что никто не выкинет недокументированный (потому что swagger документация требует явно описывать все возможные ошибки и их модели).


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



public class Person(string FirstName, string LastName)
public class Person { string FirstName; string LastName; }
public data class Person { string FirstName; string LastName; }
public class Person { string FirstName {get;set;} string LastName {get;set;} }
public class Person { string FirstName {get;init;} string LastName {get;init;} }
public class Person(string firstName, string LastName)

Welcome, C# 9.0 Семимильными шагами догоняет плюсы.

Насколько я понимаю, способов инициализации для потребителя всё ещё два, а с точки зрения CLR — так и вовсе один; всё перечисленное вами — всего лишь разные реализации.


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

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

Соглашусь, лично мне местами изменения кажутся сомнительными, особенно сравнивая с Scala, где те же case classes выглядят как-то попроще. С nullability вообще грустно, но что поделать, наследие.
Насчёт синтаксиса было смешно: data class решили упростить до просто record, таким образом в ближайшее время планируют public record Person. На структуры не работает, но если таки допилят и для них, то обновят и надо будет писать public record class Person. В обсуждении этого дела успели набрать ~50 комментов .

  • Higher order functions
  • Closures
  • Map/reduce и прочая
  • Вывод типов
  • Generics в противовес c++ templates

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

Скорее этими причинами будет рантайм, стандартная библиотека и фреймворки, тулинг в виде IDE, систем сборки, мониторинга

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

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

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

Линия рассуждений простая — ФП когда-то было очень хайповой темой. Академики и энтузиасты сделали кучу языков, часть из них стала немного популярна. В этих языках обнаружились фичи, которых в мейнстриме нет. За 15 лет самые полезные из них заимствованы мейнстримом. Упомянутые уже higher order functions, type inference и т.п.


Вы оппонируете и приводите пример несколько фич, которых в условном C#, Java, Swift нет. На это я могу сказать, что а) всегда есть к чему стремиться и мейнстрим подтягивается и б) с учётом всех фич, которые уже перетекли в мейнстрим, ФП просто нечего уже предложить. В каком-нибудь OCaml есть куча всего, но 50% наиболее ценного из этого уже есть в C#. А оставшиеся 50% никак не перевешивают проблем с IDE, рантаймом и прочим.


Резюмируя — у ФП не осталось киллер фич, можно сказать, что оно устарело.

Резюмируя — у ФП не осталось киллер фич, можно сказать, что оно устарело.
Ну… можно и так сказать.

На самом деле тут вот в чём беда: ФП, так-то, изначально решает проблему, про которую кажется, что она нужна всем — а на самом деле не нужна почти никому.

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

Ещё больше людей — которые это не произносят явно, но чувствуют интуитивно.

А в тех редких случаях, где корректность, всё-таки, важна и нужна — мы можем посадить 100500 экспертов и выверить каждую строку кода (это неверно, на самом деле, но считается что так оно должно и будет работать).

И вот в этой парадигме — ФП действительно нечего больше предложить мейнстриму. Но будет ли так всегда?.. Как ни странно ответ на этот вопрос — «не знаю». Честно. Я действительно не знаю.

Но если, вдруг, люди реально захотят сделать так, чтобы их программы были корректными и работали надёжно… о, в этом случае ФП вполне ещё может выстрелить.
За 15 лет самые полезные из них заимствованы мейнстримом. Упомянутые уже higher order functions, type inference и т.п.

Тот же type inference как в мейнстриме (в плюсовом auto, сишарповом var и так далее) весьма отличается от вывода типов в ФП.


А оставшиеся 50% никак не перевешивают проблем с IDE, рантаймом и прочим.

Во-первых, оставшиеся 50 — это ADT, это GADT, это подъём термов на уровень типов, это тайпклассы, это частичное применение, это контроль за эффектами, это отсутствие синтаксического шума, в конце концов.


Во-вторых, вот я прям сейчас в соседнем окне сижу пишу код на хаскеле. Какие проблемы с IDE и рантаймом я, по-вашему, наблюдаю?


Проблем с C++ IDE у меня на порядок больше, и если бы не CLion, то более-менее адекватной IDE не было бы совсем.

Про IDE, возможно, речь идёт в сравнении с visual studio + C#. Мне изредка приходится что-то править в шарповом коде, и VS действительно отлично помогает. Благодаря очень удобному повсеместному автодополнению можно с самым базовым знанием языка писать и править блоки кода, которые даже будет работать.
Ни для C++, ни для Python, ни для Haskell у меня такого ощущения в разных IDE не было.

Ну фиг знает. Сейчас пишу в Idea с IntelliJ-Haskell — о некоторых вещах желать можно, но не из C#-мира, а, что иронично, из идрис-мира (типа case split или proof search — как там с этим у C#?).


Впрочем, мой опыт с C# ограничивается единственным проектом семилетней давности (привет одной компании в районе ВЦ РАН, кстати), так что, может, я просто не знаю, чего можно желать.

Ну поставьте и сравните. Не все рефакторинги которые есть в шарпе есть в хаскелле, конечно, но не все рефакторинги нужны — многие вещи решаются языковыми средствами, а не "решарпер, сгенерируй мне, пожалуйста IEquitable, IComparable и десяток операторов сравнения, потому что язык не умеет в дерайвы".

Не понял, что именно вы утверждаете, что поставить и сравнить? Про рефакторинги я не говорил, и решарпером не пользуюсь.
Вот же:
Благодаря очень удобному повсеместному автодополнению можно с самым базовым знанием языка писать и править блоки кода, которые даже будет работать.

В те редкие моменты, когда мне приходится что-то править и дописывать на C#, получается достаточно быстро разобраться, как это сделать. Методом тыка в автодополнение в разных местах быстро находятся нужные методы. Конечно, дополнение есть во многих IDE для разных языков, но в VS + C# лично мне оказывается удобнее всего, что видел.

Так автодополнение даже в идрисе есть, который 1.5 студента пилят, в хаскелле — тем более.


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


img

Опять не понимаю, с чем вы спорите. Вот я же пишу прямо в предыдущем сообщении:
Конечно, дополнение есть во многих IDE для разных языков, но в VS + C# лично мне оказывается удобнее всего, что видел.

Так в чем это удобство заключается, можно узнать? По пунктам?


Потому что необходимость нажать 2 кнопки 1 раз после установки ИДЕ это явно не та сложность которая хоть на что-то влияет.

Так в чем это удобство заключается, можно узнать? По пунктам?

Нет, нельзя — разница в удобстве, как обычно, субъективная.
Потому что необходимость нажать 2 кнопки 1 раз после установки ИДЕ это явно не та сложность которая хоть на что-то влияет.

Какие две кнопки вообще? Я про общее удобство и качество автодополнения.
Нет, нельзя — разница в удобстве, как обычно, субъективная.

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


Какие две кнопки вообще? Я про общее удобство и качество автодополнения.

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


Что ещё нужно?

Во-первых, оставшиеся 50 — это ADT, это GADT, это подъём термов на уровень типов, это тайпклассы, это частичное применение, это контроль за эффектами, это отсутствие синтаксического шума, в конце концов.


Если все это в C#/Java окажется завтра я буду только рад. Мой тезис не в этом, а в том, что самое вкусное уже есть, то что осталось это тоже вкусно, но уже не там важно как остальное.

Из перечисленного type class решают важную проблему (expression problem) я чего-то подобного очень жду. Я выделяю эту фичу именно потому, что есть expression problem, часто встречается на практике, решения в C#/Java хорошего не имеет.

Какие проблемы с IDE и рантаймом я, по-вашему, наблюдаю?

В этом смысле Java впереди планеты всей. Хочется что-то подобное JMX, профилировщики, тюнинг GC, remote отладку, отладку core dump, возможность подключить что-то вроде new relic. В какой-то степени это обычно есть, но если вы пользовались отладчиком или JMX в Java, то вы меня поймете — это все не то. Не хватает возможностей, легкости настройки и самое главное — стабильности.

Из чисто runtime фич это хороший JIT, возможность генерить код в рантайме, быстрый reflection. Всякие важные мелочи, вроде lock — который использует CAS когда можно и переключается на примитивы ОС только если потоки начинают за него бороться. Хороший thread pool/work stealing queue тоже не в каждом языке найдется.

Проблем с C++ IDE у меня на порядок больше, и если бы не CLion, то более-менее адекватной IDE не было бы совсем.

Для меня до сих пор загадка как такой франкенштейн как C++ вообще дожил до наших дней. И это при том, что во время его изобретения уже были common lisp, smalltalk, pascal, ada.
Из перечисленного type class решают важную проблему (expression problem) я чего-то подобного очень жду. Я выделяю эту фичу именно потому, что есть expression problem, часто встречается на практике, решения в C#/Java хорошего не имеет.

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


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


В этом смысле Java впереди планеты всей. Хочется что-то подобное JMX, профилировщики, тюнинг GC, remote отладку, отладку core dump, возможность подключить что-то вроде new relic.

Мне на хаскеле не надо было отлаживать core dump ну вот ни разу (кроме тех случаев, когда я делал FFI в libclang, а libclang очень, гм, интересная библиотека, но это другой разговор). Короче, отладка не всегда нужна, на самом деле.


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


Из чисто runtime фич это хороший JIT, возможность генерить код в рантайме, быстрый reflection.

Это тоже, видимо, субъективно. Генерить код в рантайме мне было нужно, но эту генерацию нужно было очень сильно контролировать, и брался LLVM и генерировался LLVM IR. JIT — ну фиг знает, зачем, если есть AOT? Reflection… Тоже, видимо, от задач зависит.


Для меня до сих пор загадка как такой франкенштейн как C++ вообще дожил до наших дней. И это при том, что во время его изобретения уже были common lisp, smalltalk, pascal, ada.

Потому что производительность.

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

Неа, не получится. Там, если делать в лоб, то будет как-то так (но не скомпилируется и правильно сделает):


class (Actor a, Weapon w) => ActorWeapon a w
instance (Actor a, Weapon w) => ActorWeapon a w
instance (Actor a, SwordLike w) => ActorWeapon a w
instance Weapon w => ActorWeapon Frog w
instance Actor a => ActorWeapon a Lance
instance Magic w => ActorWeapon Dragon w

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


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

Там, если делать в лоб, то будет как-то так (но не скомпилируется и правильно сделает)

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


я, кстати, так и не нашел набора флагов, который позволил бы то решение собрать

Всего-то.

В этом смысле Java впереди планеты всей. Хочется что-то подобное JMX, профилировщики, тюнинг GC, remote отладку, отладку core dump, возможность подключить что-то вроде new relic. В какой-то степени это обычно есть, но если вы пользовались отладчиком или JMX в Java, то вы меня поймете — это все не то. Не хватает возможностей, легкости настройки и самое главное — стабильности.

Знаете, я в хаскель чате общался с людьми насчет дебага, так вот там есть люди с 10+ годами опыта промышленной разработки на хаскелле, и они не знают как работать с дебаггером. Ну то есть они не слышали что такое watch window, что такое step over и step into. Казалось бы, невозможно, неверноятно, но — факт...


Что до кодогенерации в рантайме — кодогенерация в дизайн тайме куда лучше, но насколько я знаю, в джаве её нет.

Я выделяю эту фичу именно потому, что есть expression problem, часто встречается на практике, решения в C#/Java хорошего не имеет.

По вашему, object algebras — это не хорошее решение?

Object algebras это хорошо замаскированный бред сивой кобылы. Смотрите, вся идея object algebra заключается в том, что есть некая функция
<A> A parseExp(IntAlg<A> f, String s)

parseExp парсит s и строит операцию или объект с данными типа A.

Когда им нужно расширить иерархию объектов они говорят
interface IntBoolAlg<A> extends IntAlg<A> {...}

Это означает, что в иерархии появился объект тип bool. И тут обман — авторы везде используют exp и exp2, где выражения hardcoded. Но они «забывают» указать, что вместе с IntBoolAlg должна изменится и сигнатура
<A> A parseExp(IntBoolAlg<A> f, String s)


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

В итоге, все что они говорят это «давайте у нас будет метод, который обходит внутренности объекта и строит операцию над этим объектом». Но это можно и без всяких алгебр сделать
T BuildAccountOperation<T>(IOperationBuilder<T> builder, AccountType entity)
{
    return entity.Type switch 
    { 
        case 1 => builder.Build1(entity), 
        case 2 => builder.Build2(entity);
    };
}

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

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

В случае с полиморфизмом клиенты знают о типе Exp и о том, что у него есть метод Eval. Также они знают об операции «Exp Parse(String)» При добавлении Bool клиенты о его существовании даже не догадываются.

В случае с алгебрами клиенты знают о IExpAlg и Parse(IExpAlg, String). При добавлении Bool они должны изменится, так как меняется интерфейс парсера на Parse(IBoolAlg, String).

Ну так Main(3) это новый клиент. А уже существующий клиент ( client.dll) менять не надо. Как и в первом случае. А то почему-то использований BoolExp в пером коде — нигде нет, а во втором BoolEvalAlg — есть. Неэквивлентные вещи.


Ну и у вас в целом неправильно: Parser нельзя трогать, нужно писать BooleanParser и уже с ним всю фигню мутить.

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

Во втором примере то же самое. Но как только в входной строке появляются bool, мне нужно использовать bool parser, а заодно перекомпилировать клиента. Если я напишу BoolParser, то мой клиент от этого с bool работать не научится.

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

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


У вас единственный ParserImpl : IParser, конечно на одной реализации такое не показывается.


Покажите на более простом примере. Есть у нас классы треугольник, круг и квадрат. У них есть функции вычисления площади и периметра. Покажите, как у вас выглядит добавление трапеции и функции печати имени на экран.

Так это потому что у вас одна реализация ParserImpl.


Так больше одной реализации и не нужно. Ее задача взять строку и построить Exp. А уже у Exp есть метод Eval. В этом заключается разница с алгебрами. Там парсер нужен чтобы построить объект, у которого есть метод Eval.

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


Хватит добавления трапеции, ровно тот случай, когда ломается visitor
interface IAlg1<T> //вам это не напоминает visitor?
{
    T Square(int x);        //T Accept(Square s);
    T Circle(int r);        //T Accept(Circle c);
    T Rect(int w, int h);   //T Accept(Rect r);
}

interface IAlgAlternative<T> //те же яйца вид сбоку
{
    T Lit(int x);
    T ShapeType(ShapeType t); //Square, Circle, Rect
}

public static class Parser1
{
    public static T Parse<T>(IAlg1<T> a, string s);
}

public static class Client
{
    public stati void Main(string[] agrs)
    {
        Console.WriteLine(Parser1.Parse(new PrintAlg1(), args[0]));
    }
}

interface IAlg2<T> : IAlg1<T>
{
    T Trapezium();
}

public static class Parser2
{
    public static T Parse<T>(IAlg2<T> a, string s);
}
//All clients has to use Parser2 or they won't be able to interact with  Trapezium shapes.
Так больше одной реализации и не нужно.

Я и говорю, что не поняли.


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

Положим я добавил трапецию, но моя графическая библиотека не сможет ее нарисовать, потому что у нее нет «parseExp(AlgWithTrapeze a, string s)». Так получается?

Вы либо ничего не можете рисовать, либо всё можете рисовать. Это расширение в сторону objects и расширение в сторону features.


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

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


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

Нет, не меняется, потому что вы неправильно поняли как работают object algebras

Я расширил набор объектов, добавил bool. Каким образом «A parseExp(IntAlg f, String s)» сможет работать с bool? А что будет со всеми клиентами этой функции?

Монады позволяют создать очень абстрактную абстракцию, позволяющую унифицировано работать с разными вещами, а не реализовывать отдельный синтаксический велосипед на уровне языка для async/await, Option итп

А программисту до фонаря как это теоретик называет.
Как раз нет — и промисы это как раз прекрасный пример. Ибо работать с ними неудобно — и именно потому что их сделали не как монады, а «как смогли».
UFO landed and left these words here
Ну кстати, в Java 8 сделали Optional — и тоже не как монаду. С тем же примерно эффектом — неудобно. Причем теоретики предупреждали заранее, что будет — не послушали.

А там-то какие законы нарушены?


Насколько я знаю, в случае Optional проблемы наблюдаются скорее с тем, что в языке нет ни сопоставлений с образцом, ни do-нотации, ни возможности запретить null-значение для Optional...

Ну, в какой-то степени все нарушения связаны с null, да (насколько я помню). То есть, оно почти выполняет законы, но только почти. Как только появляются нуллы — так сразу нет.

Join нельзя определить на нуллябельном Option. А значит и эндофунктора нет.

А что мешает сделать с нуллами то же самое, что обычно делают с боттомами — притвориться, что их не существует?


Если в языке любой пользовательский тип данных неявно расширен значением null — это печально, но это же не означает что теперь недопустимо даже пытаться ввести Optional?

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

Дело не об этом, дело в том, что вы не можете вкладывать опшны друг в друга. Нельзя сделать Option<Option<null>>. А это уже нарушает законы (потому что должен быть моноид в категории эндофункторов, а эндофунктором он быть не может потому что не на всех значениях определён).

А кто мешает вкладывать их друг в друга? Тот же Option<Option<null>> я не могу сделать потому что null, а вовсе не из-за вложенности.

Я об этом и говорю, вы не можете сделать потому что уже null, и я не знаю как в джаве, но в дотнете даже такой тип объявить не получится:


img

Ну так Option из Java и Nullable из C# — это совершенно разные вещи, созданные для разных целей.

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

По первой ссылке пишется явно корявый код (про который по второй ссылке говорится, что он "should never have existed"), а потом демонстрируется, что к нему трудно прикрутить Optional. Вот только к нему что угодно трудно прикрутить, ибо он коряв изначально.


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

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

Я к сожалению не нашел ссылки на нормальное обсуждение, которое было еще до релиза Java 8 в блоге у разработчиков. Эти два да, так себе.

И вот тут еще про нарушение конкретно. А именно, почему попытки сделать null-safe нарушают например левую ассоциативность.

То что Optional нарушает монадные законы — это ясно. Что это, по-идее, плохо — тоже (я сам дважды имел неприятности из-за отсутствия ассоциативности там, где априори она ожидалась — с другими недомонадами, не с Optional).


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

>не приводит к реальным проблемам.
Ну смотрите, отсутствие ассоциативности приводит к тому, что сделав якобы рефакторинг — то есть такое преобразование, которое не должно бы изменять семантику — мы де факто получим ее изменение. То есть, например, map(f).map(g) и map(f.andThen(g)) может иметь не идентичное поведение — то есть замена двух мапов одним и композицией функций может наш код сломать. А может и не сломать.

Не то чтобы Optional вообще пользоваться было нельзя — это конечно не так, но определенные проблемы оно вызывает.

Частая проблема — написание генерик кода. Например, мы делаем парсинг JSON'а (в общем виде), и у нас есть функция getJsonValueByKey(string key) которая возвращает нам этот Option<TValue>. Вопрос, а что нам делать, если сам TValue — это опциональное поле (например, goods_weight Option<Double>)?

Не вполне понятно откуда в JSON мог взяться Option, но вернуть Option<Option<Double>> в Java ничто не мешает.

Значит я не так понял проблему.


Он мог оттуда взяться из проблемы например исполнения команды PATCH (или аналога). То есть вопрос, как делать PATCH на нуллябельное значение.

async/await это и есть монады, у них даже интерфейс похож до степени смешения. Нет удобной do-нотации и потому эта фича не используется для реализации штук вроде Maybe.

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

Заголовок спойлера
image
Почему функциональное программирование такое сложное
Потому что его пытаются противопоставлять всем остальным парадигмам и натягивать даже на те задачи, для которых он не подходит. Например на те, которые изначально императивные и state-full в силу своей бизнес-логики.
Точно так же, к ООП становится дико переусложнённым, когда на нём пытаются сделать, например, поточную state-less логику обработки данных.
Основная сложность функционального программирования главным образом заключается в стремлении своих адептов рассказывать максимально сложно даже про весьма простые вещи. Почему и за счет каких поведенческих механизмов так происходит — отличная пища для размышления.
Потомучто эти адепты боятся хоть в запятой ошибиться при объяснении простых вещей. Ведь тогда другие такиеже адепты их заклюют и выбросят из своей культуры.
// Этот полу-магический эффект, «если скомпилировалось, значит работает», замечают практически все, кто изучает ФП.

Помнится, когда я перешел с Бейсика на Си++, я тоже был поражен — возиться с компиляцией куда дольше, но зато работает куда лучше. Лучше, но не идеально.

Уже не так давно юзал Хаскель — да, компилится и работает. Лучше, но не идеально.

Грубо начиная с 1000 строковых программ компиляция перестает гарантировать работоспособность. ООМ из-за ленивости языка, старые хвосты вроде функции head(кидающие исключение на пустой список) в коде какой нибудь либы, баги в коде который работает с битами напрямую из соображений производительности…

И, прежде всего — семантические ошибки, которые пролезают в повышенной дозе как раз из-за того что уже выработалась привычка что скомпилировавшийся код тестировать не надо.
Да там одни runtime-ошибки из-за пропусков при pattern matching чего стоят :) Про OOM из-за неотработавшей оптимизации хвостовой рекурсии я, пожалуй, промолчу — нельзя считать инструмент, провоцирующий подобные ошибки надежным и/или безопасным даже в первом приближении.

Зато там паттернов пропущенных не будет, да и бесконечных циклов рекурсий тоже.

А зачем тесты? Будто в тестах никто не ошибается?


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

Семантические ошибки тоже можно отлавливать с помощью типов, оборачивать разные семантические типы в newtype, явно формулировать ограничения и эффекты.
И какая-то практика тоже нужна, чтобы заранее избегать ловушек вроде let Just x = ....
Опять же, кто вам запрещает писать тесты?

Ок, как отлавливается опечатка "+" вместо "-" в формуле с помощью типов?

Как отлавливается ошибка в порядке вызова функций с помощью типов (например rotate, translate вместо translate, rotate в графике)?

Это реальные ошибки, которые я лично делал.

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

Пред и постусловиями. Их можно в типах сформулировать, как в Idris/F*/ATS, в императивных языках со слабой системой типов (например, Си) можно воспользоваться predicate transformer semantics (похожее делают в FramaC).

Ну например вот так:


add : Nat -> Nat -> Nat 
add a b = a - b

.\.\test.idr:53:13:
   |
53 | add a b = a - b
   |             ^
When checking right hand side of add with expected type
        Nat

When checking argument smaller to function Prelude.Nat.-:
        Can't find a value of type
                LTE b a

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

А если без сигнатуры? Вроде же недавно хвастались, что церемоний можно избежать...

В топ-левел декларации обязательно типы надо указывать (и правильно), для локальных там уже будут свои приколы (тем более, что минус перегружен для Nat и для Neg ty), но если просто написать add a b = a - b то конечно никакой компилятор не остановит.

В топ-левел декларации обязательно типы надо указывать

То есть от опечаток защищены только топ-левел функции? Ну ок :-)

А кто вам в ассемблере запрещает писать идеальный код?

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

Отсутствие хорошей системы типов, конечно!

Прочитал. Спасибо, весьма интересно.
Но вот не понятна война адептов ФП с ООП ( именно в таком ключе ). Мне кажется с тем-же ООП можно применять подходы ФП.

Отсюда: https://ru-lambda.livejournal.com/27669.html .


Как-то однажды знаменитый учитель Кх Ан вышел на прогулку с учеником Антоном. Надеясь разговорить учителя, Антон спросил: "Учитель, слыхал я, что объекты — очень хорошая штука — правда ли это?" Кх Ан посмотрел на ученика с жалостью в глазах и ответил: "Глупый ученик! Объекты — всего лишь замыкания для бедных."


Пристыженный Антон простился с учителем и вернулся в свою комнату, горя желанием как можно скорее изучить замыкания. Он внимательно прочитал все статьи из серии "Lambda: The Ultimate", и родственные им статьи, и написал небольшой интерпретатор Scheme с объектно-ориентированной системой, основанной на замыканиях. Он многому научился, и с нетерпением ждал случая сообщить учителю о своих успехах.


Во время следующей прогулки с Кх Аном, Антон, пытаясь произвести хорошее впечатление, сказал: "Учитель, я прилежно изучил этот вопрос, и понимаю теперь, что объекты — воистину замыкания для бедных." Кх Ан в ответ ударил Антона палкой и воскликнул: "Когда же ты чему-то научишься? Замыкания — это объекты для бедных!" В эту секунду Антон обрел просветление.

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

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

Спасибо за статью, первая часть очень понравилась, где мотивация расписана, но вот начиная с определений пошла какая-то жара.


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


По фактическим ошибкам — многое написали выше, я только повторюсь в одном: категория это НЕ типы. В частности, если вы читали про категории вообще, то в рамках программирования рассматривается категория Hask — то есть категория типов Haskell (ну, она равномощна категории типов любого языка, ведь по сути это просто Set с боттомами, потому что в реальном мире вычисления могут зависать). А объекты этой категории — уже типы. Соответственно, примером морфизма в нашем случае можно взять функцию из Int в Bool. Какую-нибудь одну, ведь множество морфизмов из Int в Bool содержит 2^(2^32) элементов.


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


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

ведь по сути это просто Set с боттомами

Вообще-то уже без боттомов:


Because of these difficulties, Haskell developers tend to think in some subset of Haskell where types do not have bottom values. This means that it only includes functions that terminate, and typically only finite values.
Ну написанное пером уже вырубить к сожалению не получится. Но я с удовольствием приму конструктивную критику и внесу изменения для тех, кто придет читать статью позже.
Буду благодарен за уточнения, иные аналогии в определениях.
Ну похоже, что автор топит не за все ФП, а лишь за ту его нескромную часть, вокруг которой сейчас так много хайпа. В этой части слишком много не особо очевидных вещей, чтобы можно было пытаться их одолеть без теории вслепую, силами одной лишь практики. Чтобы проектировать хотя бы на уровне монад, придется разобраться в монадических законах. А для этого, по моим представлениям, надо осилить примерно четверть теорката. Или вместо монад получится каша в голове и промисы в коде. Чтобы осмысленно комбинировать/трансформировать монады, придется научиться жонглировать функторами и натуральными преобразованиями. Это еще небольшой кусочек примерно на месяц. Где-то через погода
в бодром темпе можно добраться до линз.
Чтобы проектировать хотя бы на уровне монад, придется разобраться в монадических законах. А для этого, по моим представлениям, надо осилить примерно четверть теорката

Здесь основная ошибка в логике. Чтобы освоить монады, не нужно не то что четверть теорката, а вообще ничего из него. Достаточно обычного ООПшного опыта минимального. Впрочем, как с оопшной точки зрения оно выглядит я уже оформил в статье, большего к этому мне добавить пока нечего.


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


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

Хорошая статья, еще раз спасибо.
Трансформеры и остальное нужны только если у вас ФП язык и вам нужно комбинировать эффекты.

Когда у вас только одна монада, то освоить bind/fmap обычно не составляет труда. Но когда монад хотя бы две, то вопрос типа как из `List<Either<e, a>>` сделать `Either<e, List<a>>` появляется почти сразу, а ответ уже не сильно очевиден. Ну или `Right Task List Right Task User a` привести к вменяемому виду.

С двумя всё тоже просто — есть traversable в 99% случаях. А больше двух в обычных языках встречается очень и очень редко.

Спасибо если в статье я хоть что-то понимал, то прыжок в комментарии выветрил остатки понимания. _)
В следующий раз попробуйте вместо цикла for написать map/forEach

for (let item of [1, 2]) {
  alert(item)
}

[1, 2].forEach(alert)

Так действительно лучше.

for (let item of [1, 2]) {
  item++
  alert(item)
}

[1, 2].forEach(item => {
  item++
  alert(item)
})

А вот так уже совсем не лучше. Первый вариант мне даже больше нравится.

Только к функциональности это имеет мало отношения. Ближе — писать флатмапы вместо циклов (выдержка из одного моего сниппета):


const orderIdToRouteInfo = routes.reduce((map, route) => {
  flatMap(route.routePoints!, (rp) => rp.orderTasks!.map((ot) => ({rp, ot}))).forEach(({rp, ot}) => {
    map[ot.orderId] = {route, routePoint: rp};
  });
  return map;
}, asType<StringMap<{ route: Route; routePoint: RoutePoint }>>({}));

И сразу видно, что писать в иммутабельном ФП стиле на ЖС весьма много словно и не очень удобно (впрочем, гарантии предоставляемые иммутабельностью мне важнее в данном случае)

Интересно, о каких гарантиях иммутабельности речь, когда тут иммутабельного кода — всего одна строчка, а остальное — замаскированный мутабельный цикл?


Неужели вот так менее понятно?


const orderIdToRouteInfo = (() => {
    let map = <StringMap<{ route: Route; routePoint: RoutePoint }>>{};
    for (const route of routes)
        for (const routePoint of route.routePoints!)
            for (const ot of routePoint.orderTasks!)
                map[ot.orderId] = { route, routePoint }
    return map;
})();

Возможно, я излишне использую это где не надо. Но я много раз обжигался на жсовых for (те же of/in и прочее), поэтому стараюсь ими не пользоваться.

Не пишу на JS с 2015 где-то года (вот тут небольшой проектик пришлось на ноде написать, потом что на дотнет либы нужной не нашлось), поэтому я не разбирался во всех нюансах современного жсо-строения, просто ещё тогда выработал привычку не трогать циклы, и с тех пор не было возможности/мотивации переучиться.

[1, 2].map(item => item+1).forEach(alert)

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

Правда в JS map не ленивый, поэтому производительность страдает, но это уже не является предметом обсуждения

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

Сложность линейная, просто константы разные.


В коде выше map не материализует промежуточный список целиком?

Материализует, конечно. Вопрос в том, будут ли альтернативы получше (в JS). Итераторы, к примеру, на каждом цикле создают объект. Непонятно, ленивые вычисления по константе будут лучше или хуже

Ну, например, если у вас нет потребности создавать весь объект в JS-смысле целиком, а достаточно указателя-другого, и всё это оседает в L1-кеше процессора (потому что за счёт ленивости один и тот же элемент сначала мапится, потом foreach'ится, потом ещё что-нибудь), то с константой всё будет хорошо.


И это я уж не говорю о компиляторе, который за счёт семантики языка гарантирует, что map (f . g) и map f . map g означает одно и то же, поэтому два map можно заменить одним (по крайней мере, если забыть про боттомы).

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

Правда в JS map не ленивый, поэтому производительность страдает

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

Сложность ФП — всякие монады, стрелки, функторы это всего-лишь отображение сложности предметной области. В классическом императивном ООП существуют точно такие-же сложности, просто они не решаются явно. И в итоге это всегда всплывает и становится огромной архитектурной проблемой.
Как пример — классические ОРМ вроде hibernate. Вроде всё просто и понятно, есть таблицы и маппинг. Можно дернуть get для получения значения свойства, set для установки. Но это только в вакууме. В реальном мире сложности начинаются сразу же — как только появляется слой бизнес логики, становится понятно, что полученные красивые объекты невозможно использовать в бизнес логике, а их «красивость» только мешает. Что lazy загрузка ломает всё в самом неподходящем месте. В итоге даже обычный CRUD превращается в головную боль.
всякие монады, стрелки, функторы это всего-лишь отображение сложности предметной области.


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

Моделировать бизнес с помощью функций это прямой путь в дурку. Моделировать бизнес с помощью объектов это прямой путь в дурку. Моделировать бизнес с помощью микросервисов это прямой путь в дурку...


Это всё эквивалентные утверждения

UFO landed and left these words here
UFO landed and left these words here

А можно пример закона физики, который не записывается в виде формул?

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

Орбита Меркурия не является законом. Но любая модель орбиты Меркурия — закон, который сформулирован в виде формулы.

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

Любое частное явление может быть выражение формулой. v=0 формула? а v=1 формула?
Но я бы не назвал это законами.

И что значит «любая»? Он пыргает с 1 на другую? У него нет любой, она 1 и постоянно смещается.
Не, ну если так подходить к вопросу «Скажите, какие из сформулированных человеком законов, которые обычно выражаются в математической нотации, не могут быть выражены в математической нотации», то вопрос вобще смысла не имеет. Все сформулированные человеком законы это попытка описать наблюдения при помощи слов и крайне желательно формул, потомучто без формул закон просто не имеет смысла, не имеет никакой предсказательной силы.

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


Но я не знаю, какие альтернативные формулировки понятия закона вообще могут быть.


Любое частное явление может быть выражение формулой. v=0 формула? а v=1 формула?
Но я бы не назвал это законами.

Э, ну из «любой закон описывается формулой» не следует, что «любая формула описывает закон». По крайней мере, с точки зрения формальной логики не следует.


И что значит «любая»? Он пыргает с 1 на другую? У него нет любой, она 1 и постоянно смещается.

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


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

И что значит «любая»? Он пыргает с 1 на другую? У него нет любой, она 1 и постоянно смещается.

Не было никакой проблемы описать орбиту меркурия формулой — даже тогда, когда эта формула не следовала из известных законов физики. Пронаблюдали положение достаточное количество раз, подогнали под точки кривую некоторого разумного вида — вот вам и формула. Величину прецессии его орбиты посчитали до теории относительности, как раз описав движение планеты формулой.
UFO landed and left these words here
первый закон Ньютона,

… является частным случаем второго для F=0.

UFO landed and left these words here
UFO landed and left these words here

Вы специально меняете свои комментарии после того как я на них отвечу?

UFO landed and left these words here
в формулу не внести понятие "любой газ"

В формулу логики первого порядка вполне внести.

любые газы при одинаковых условиях (объём/давление/температура) содержат одинаковое количество молекул.

А вы не задумывались, что это и есть формула, только записанная в нотации естественного языка?
UFO landed and left these words here

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

Вот этот который?
img


Который так же является следствием известного F = ma

UFO landed and left these words here

Это вы записали, что "тела, на которые не действуют силы, движутся равномерно". А первый закон — "существуют СО, такие что в них тела, на которые не действуют силы, движутся равномерно".

UFO landed and left these words here

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


И вопрос "Существуют такие СО" — а какие ещё существуют? Насколько я знаю, в нашей бренной вселенной никаких других и нет.


Наконец, если вам так хочется то допишите ∃world, ...

Ну, википедия она такая. Даже английская. Лучше загляните в хороший учебник. Это не хороший учебник, но здесь есть главная идея первого закона:


Первый закон Ньютона (или закон инерции) из всего многообразия систем отсчета выделяет класс так называемых инерциальных систем.

и собственно правильная формулировка:


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

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


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

закон сохранения энергии

Что, простите? В самом простом виде это либо E=const, либо dE/dt = 0.
UFO landed and left these words here

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

UFO landed and left these words here

Какой "частный случай"? Просто постулируется, что F = ma. Если сил нет, то ускорение — нулевое. Или вам не хватает окружения в стиле "наш мир устроен таким образом, что в нём верны вот такие вот формулы"? Ну, это называется контекстом. Вся эта лабуда про "инерциальные системы отсчёта" — это просто словесная шелуха.

UFO landed and left these words here

"Первый и второй закон" это всё про исторические совпадения, не более того. Полагаю, как раз по причине нелюбви европейцеп к понятию нуля.


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

UFO landed and left these words here

Ну так про что угодно можно сказать. Вон, вы говорили что "Бойля-Мариотта сводятся к формулам", но ведь там то же самое:


При постоянных температуре и массе газа произведение давления газа на его объём постоянно.

Ага, но в формулах нет ни слова про "газ", и попытки применить его, к, скажем, твёрдому телу, провалятся. Полуачается, и там к формулам не сводится? Если применить уравнение маятника не к маятнику, то тоже фигня выйдет. Ну и так далее.


Тогда физику вообще математикой описать нельзя, вообще ничего из неё, ведь какую формулу не возьми, там всегда словесный контекст, про что идет речь: газ, твердое тело, что речь идет про планету Земля (с определенными условиями в виде температуры/давления/...), и так далее.