Comments 42
Каждый раз, когда читаю про тру кошерный правоверный ФП, радуюсь, что Джаву и родственный Котлин делают дальновидные люди. Затащили в язык ровно столько ФП, сколько нужно для реальных потребностей. А ведь у Jetbrains явно руки чесались затащить побольше.
Каждый раз, когда читаю про тру кошерный правоверный ФП
То самое тру кошерное правоверное ФП на джаве...
радуюсь, что Джаву и родственный Котлин делают дальновидные люди. Затащили в язык ровно столько ФП, сколько нужно для реальных потребностей. А ведь у Jetbrains явно руки чесались затащить побольше.
ЯП неприспособлен к ФП ->
ФП в нём выглядит плохо ->
"Правильно неприспособили, посмотрите как ФП плохо выглядит"
К слову, посмотрите как-нибудь как выглядит реализация ООП в том же си
Примерно так, да. Оно возникло вот почему: берём Haskell, в котором всё иммутабельное, начинаем писать код. Из-за иммутабельности код становится скучным и многословным: любой цикл превращается в рекурсию, и добавить какое-то действие в цикл означает добавление параметров в рекурсивную функцию. Через какое-то время вас достанет писать однообразные рекурсивные перекладывания из списка в список, и вы придумаете fmap. Аналогично с монадами: очень утомляет делать цепочки действий и на каждом шаге выполнять одни и те же вспомогательные операции, научились их прятать. Если у вас нет иммутабельности и есть циклы, то таких проблем не возникает.
Из-за иммутабельности код становится скучным и многословным: любой цикл превращается в рекурсию, и добавить какое-то действие в цикл означает добавление параметров в рекурсивную функцию.
Не понял, зачем? Если у вас всё пока что иммутабельное, то не нужно ничего никуда добавлять. Замыкания, все дела:
addEach :: Int -> [Int] -> [Int]
addEach n = go
where
go [] = []
go (x:xs) = x + n : go xsЧерез какое-то время вас достанет писать однообразные рекурсивные перекладывания из списка в список, и вы придумаете fmap.
fmap работает на произвольных функторах, а вот конкретно на списках map был ещё в лиспе в бородатых 60-х, когда хаскеля и в проекте не было (и лисп не то чтобы славится иммутабельностью).
Интересно, зачем в C++ придумали std::transform или std::for_each? Наверное, тоже иммутабельность виновата?
Аналогично с монадами: очень утомляет делать цепочки действий и на каждом шаге выполнять одни и те же вспомогательные операции, научились их прятать.
Какие вспомогательные операции в общем случае для монад?
И почему в те же плюсы добавили монадические операции для std::optional и std::expected? Хаскелисты покусали плюсистов?
Да, я чувствовал, что моё объяснение сумбурное и непонятное, я попробую ещё раз (скорее всего, тоже получится не очень). Я хотел сказать, что когда всё иммутабельное и рекурсивное, то через некоторое время в коде появляется большое количество функций, отличающихся совсем чуть-чуть. Вот пример с addEach, потом будет substEach, multEach и прочее, не хочется иметь много маленьких фукнций, хочется обобщить, вытащив операцию в параметр, получаем map. То есть абстрагирование в функциях - естественный процесс, тупо экономия кода. А уже потом появляются общие алгоритмы, которые работают поверх таких абстрактных функций, это скорее бонус, чем начальная причина.
Я приплёл иммутабельность и рекурсию в одно объяснение потому, что в Java не используют ни того, ни другого, а обе эти штуки способствуют более активному написанию маленьких фукнций, и дальше по пути обобщения. Разумеется, фукнции высшего порядка полезны и без иммутабельности (это про std::transform и std::for_each ). Конечно, map был в лиспе, моя логика в том, что не иммутабельность языка (которой в лиспе нет) заставила придумать эту функцию высшего порядка, а, собственно, желание сделать иммутабельное поведение.
И почему в те же плюсы добавили монадические операции для
std::optionalиstd::expected? Хаскелисты покусали плюсистов?
Я думаю, что да. Эти операции не выставлены через общий для всех монад интерфейс, нет такого интерфейса, поэтому плюсовики не знают, что такое монада. Кстати, из любопытства пошёл поискать в кодовой базе плюсовиков, применяют ли они эти методы, но потом понял, что это std=23, а у нас std=20 .
Так это вы описали вообще естественный процесс абстрагирования:
Я хотел сказать, что когда всё иммутабельное и рекурсивное, то через некоторое время в коде появляется большое количество функций, отличающихся совсем чуть-чуть.
То же самое, когда в коде всё мутабельное и императивное. Появляются алгоритмы в std::, появляются ООП и паттерны всякие там, и так далее.
Эти операции не выставлены через общий для всех монад интерфейс, нет такого интерфейса, поэтому плюсовики не знают, что такое монада.
В плюсах нет дешёвого и неболезненного способа выразить интерфейс монады.
Но, впрочем, это неважно: монада не перестаёт быть монадой только потому, что вы не знаете, что пользуетесь монадой. Я когда в детстве на летней практике в школьной библиотеке считал количество сданных книг через группы по пять палочек, то не то чтобы понимал, что пользуюсь тем, что натуральные числа образуют моноид по сложению (и поэтому их можно считать группами), но моноидом они от этого быть не перестали.
Всё верно, я старался описать естественный процесс абстрагирования, чтобы объяснить автору первого комментария в этой ветке и многим, кто разделяет его мнение, что мы не хотим их обидеть это не академическая хренотень, а практичный способ уменьшить размер кода, чтобы меньше читать и писать приходилось. Было бы круто иметь примерчик "до" и "после", но нет готового.
В чем криминал наличия большого количества маленьких понятных функций, пусть даже и мало отличающихся друг от друга (почему это вообще должно парить?)?
В чем героизм наворотить систему, в которой даже сам автор через полгода не может разобраться, пусть даже (теоретически!) его поделие чуть короче?
На самом деле если разобраться на примерах попроще, то некоторые штуки будешь постоянно использовать. Тут какие то сильно сложные примеры, есть монады попроще и более полезные, тот же Try. Какой нибудь чатгпт приведет примеров
Неплохая попытка, очень нравится чувствовать опыт живого человека.
Не очень понял, почему вы назвали Function , List и Optional тайпклассами на основании того, что у них есть generic-параметры. Хаскелевский тайпкласс ближе всего к Java-интерфейсам, поэтому Function - да, тайпкласс, List - тоже, но потому, что у него есть разные реализации (ArrayList, LinkedList), а Optional - вполне себе хаскелевский data .
Лично мне всегда казалось, что проще всего объяснить, какой тип можно считать монадой, через join: например, можно же схлопнуть список списков в плоский список, значит монада, зачем это нужно - другой разговор. Вы вроде тоже зашли через join, но быстро перепрыгнули на bind, ну пусть так.
Может быть через "join" было бы проще. Мне почему-то казалось что bind - это тот основной метод, который используется для реализации монад. Возможно это не так, у меня нет реального опыта в Хаскеле. Плюс в конце хотелось подвести к IO, а там как раз используются binds разных видов
Лично мне всегда казалось, что проще всего объяснить, какой тип можно считать монадой, через join: например, можно же схлопнуть список списков в плоский список, значит монада
Парсер — это монада. Какова операционная семантика схлопывания… парсера парсеров? Парсера, возвращающего парсер? Блин, да это даже не выговоришь.
State s — это монада. Какова операционная семантика схлопывания State s (State s a)?
Вероятности — это монада. Какова операционная семантика схлопывания MonadDistribution m ⇒ m (m a)?
Есть, в конце концов, свободные монады поверх произвольных функторов, data Free f a = Pure a | Roll (f (Free f a)). Схлопнете тут?
Про Cont я и говорить не хочу, это исчадие ада.
Понятно, что это всё можно описать (и кое-что — даже весьма просто, если у вас есть достаточный опыт — скажем, я второй десяток лет развлекаюсь ФП и когда-то занимался статами, поэтому представить себе вероятностное распределение на распределениях и их маргинализацию я вполне могу). Но вы правда считаете, что это всё интуитивнее, чем делать через bind?
join, конечно, является основой, если вы начнёте заниматься теоркатом (потому что там это естественное преобразование T² ⇒ T). Но если вы всё-таки пишете код, то bind ИМХО проще.
И, возвращаясь к вашему соседнему комментарию, как наличие циклов в языке поможет избежать монад в случае парсеров или, скажем, вероятностного программирования?
Парсер — это монада. Какова операционная семантика схлопывания… парсера парсеров? Парсера, возвращающего парсер? Блин, да это даже не выговоришь.
Проекции Футамуры-Ершова?
Мысль о том, что любой парсер - это монада, является для меня новой и непростой, мне придётся внимательно посидеть над имеющимися под рукой императивными парсерами, чтобы разглядеть там монаду, поэтому я сейчас не готов ответить на вопрос, позволило ли наличие циклов избежать монад или они остались, просто я их не вижу. Про вероятности тем более не скажу, очень слаб в этой теме.
Я пытался сказать, что для джавистов или плюсовиков, которым привычно понятие контейнерного типа, объяснение "что есть монада" через join заходит проще (на мой взгляд). Думаю, это из-за того, что join - он какой-то структурный, а bind выглядит как поведенческий, потому что это функция высшего порядка, хотя если подумать внимательно, то bind - тоже структурный. Я согласен, что не для всех случаев join понятнее, чем bind.
Мысль о том, что любой парсер - это монада, является для меня новой и непростой, мне придётся внимательно посидеть над имеющимися под рукой императивными парсерами
Я не могу сказать про «любой», но если достаточно долго смотреть, скажем, на boost.spirit, то может привидеться какой-нибудь из parsec'ов, например.
Лють лютая. Что интересно,там на каждом манипуляции и логические противоерчия лишь, бы натянуть эту сову на глобус.
Интересно,конечно, но зачем?
Всё-таки ограничение функций одним аргументом и одним значением – это не свойство ФП, а особенность выбранного синтаксиса. Большинство языков ФП используют функции от нескольких аргументов, и некоторые позволяют возвращать несколько значений (хотя последнее и не является часто используемой на практике возможностью, потому что обычно проще возвращать объединённую структуру данных).
Сам формализм лямбда-исчисления подразумевает несколько аргументов.
Также как и «функция», «тип» в ФП - это более строгое математическое понятие
интересно "это более строгое понятие" чем какое?
Насколько я помню есть только одно альтернативное определение типа: тип определяется набором операций которые определены для этого типа (над этим типом). В ООП это совершенно прозрачно отображается в методы класса, поэтому класс - это тип, который определяет все свои методы-операции. И развитость системы типов - это возможность определить любой нужный вам новый тип. А что понимают фанаты ФП под заклинанием "развитая система типов" вы не достанете из них и клещами.
Есть например операция сложения она определена изначально, из арифметики, только для чисел, но ее взяли и "натянули" на строки, потому что (временами) все равно удобнее писать:
строкаА + строкаБ
чем строкаА.Добавить(строкаБ)
но операция
строкаА + целоеХ
в общем и целом никому не нужна, поэтому когда она нужна каждый измудряется как хочет, главное что у нас есть возможность эту операцию доопределить для существующих типов. Только надо помнить что
строкаА + целоеХ
и
целоеХ + строкаА
это могут быть совершенно разные операции, а могут быть и не разные, и монады тут вряд ли помогут.
А что понимают фанаты ФП под заклинанием "развитая система типов" вы не достанете из них и клещами.
Я, кажется, уже встречал треды, где вам пытались объяснить что-то на тему, но можно попробовать ещё раз.
Развитая система типов — это система, которая позволяет выразить больше проверок на уровне типов. Если на уровне типов можно выразить, что строку с числом нельзя сложить, то это, конечно, неплохо, но не топ. Если на уровне типов можно выразить понятие сортированного массива, и что ваша функция сортировки всегда возвращает сортированный массив, то это уже, конечно, развито и выразительно.
Насколько я помню есть только одно альтернативное определение типа: тип определяется набором операций которые определены для этого типа (над этим типом).
Есть ещё категориальная формулировка, вроде «типы и термы в просто типизированном лямбда-исчислении — это соответственно объекты и морфизмы в декартово замкнутых категориях» или «тип-сумма — это левый сопряжённый к диагональному функтору», но это если хочется прям строгости-строгости.
Если на уровне типов можно выразить, что строку с числом нельзя сложить, то это, конечно, неплохо, но не топ.
Вот вы все тянете в свою сторону - в сторону того чтобы было что-то НЕЛЬЗЯ. А мне надо чтобы было МОЖНО! Например, чтобы выразить, что строку с числом сложить можно. Потому что в С++ это уже нельзя, изначально, но есть возможность разрешить когда нужно.
Получается что у вас обратная парадигма - все ИЗНАЧАЛЬНО можно (пусть и с непонятными результатами), поэтому надо писать что НЕЛЬЗЯ.
Мне все таки кажется что парадигма которая изначально запрещает все операции с непонятными результатами, но позволяет их разрешать когда мы придумали для них смысл реализуется гораздо компактнее.
Если конечно у меня есть право тоже что-то объяснять.
Я влезу, извините. Вы выделили не то слово в фразе, тут суть не в нельзя/можно, а в "строка" и "число". Мощность системы типов определяется тем, насколько много и насколько детальных подробностей о величинах можно засунуть в тип и проверить на стадии компиляции. Типы "строка" и "число" содержат мало подробностей, но это лучше, чем тип "любое значение".
Изначальная же парадигма, без сомнения, что всё изначально нельзя.
Мощность системы типов определяется тем, насколько много и насколько детальных подробностей о величинах можно засунуть в тип и проверить на стадии компиляции.
Можно пример из той же Джавы? Как ограничена возможность определения и количество детальных подробностей о величинах типа string например, насколько достаточно этих ограничений? Насколько я понимаю, мы можем определить новый тип на базе того же string с еще большими ограничениями или я не прав?
Просто если я прав то получается что Джава это язык с бесконечно мощной системой типов. Это чисто формально следует из вашего определения.
Можем определить, но у строк могут быть несколько независимых признаков:
непустая строка
русская (локализованная) строка
строка без пробелов
строка введенная пользователем (и не проверенная)
строка, которую можно заинтернить (количество значений ограничено)
Если вы попытаетесь простым наследованием покрыть все варианты, у вас будет комбинаторный взрыв.
В более мощных системах типов больше гибкости для compile-time проверок
В более мощных системах типов больше гибкости для compile-time проверок
было бы очень интересно увидеть пример такой болЬшей гибкости c проверками строк хотя бы по отношению к тому что можно сделать на JAVA.
Кстати наследование ничего не добавляет, то есть дело тут явно не в наследовании, а в добавлении функций с дополнительными проверками в производный класс. Вроде как тут мы никак не ограничены, откуда может взяться "больше гибкости" все равно не очень понятно.
получается что Джава это язык с бесконечно мощной системой типов.
С точностью до наоборот. В Java весьма примитивная (в основном, конечно, в силу своей древности) система типов. Достаточно вспомнить, например, как типизируется null. Ну или сложности с введением нормальных функциональных типов и те "костылики", которые возникли в типизации (например, лямбд), после введения "аналогов".
С Java история вот какая. До Java 5 всё, что система типов предлагала, были скаляры или объекты какого-то класса, всё, других подробностей указать возможности нет. Единственным способом очень гранулярно управлять этими подробностями с целью их статической проверки было разбиение на очень детальные интерфейсы. Например, вы могли иметь ReadCollection с методами только для чтения, от которой наследуется ReadWriteCollection, у которой были бы ещё методы для записи. Статическая проверка состоит в том, что объект принадлежит классу, возможности класса определяются его методами, вы можете указать класс, выставляющий минимально требуемый вам набор методов. Так не сделали, иерархия становилась очень большой, вместо этого перенесли проверки в runtime. Но вот тип элемента у коллекции нельзя было указать. Начиная с Java 5 добавили такие подробности: и нужный набор методов в коллекции (доступ по индексу, например), и тип элемента. Всё, на этом возможности системы типов в Java заканчиваются, вы не можете декларативно добавить каких-то ограничений и статически их проверить.
Прикол Haskell по сравнению с Java состоит в том, что тайпклассы навешиваются извне, а Java-интерфейсы нужно указывать при определении класса. Это, например, как в Java вы могли бы указывать Comparable отдельно. Это позволяет придумывать свои абстракции поверх тех типов, которые вам нужны, в Java для этого придётся делать adapter-ы, запаковку+распаковку, тайпклассы легковеснее.
у меня если честно был очень эпизодический опыт использования java, и мне кстати тоже показалась она достаточно ограниченной.
Но вот насчет С++, если можно, мне действительно интересно стороннее мнение. Там же все это уже можно в последних стандартах (тайпклассы, Comparable, ...), как там оценивается мощность системы типов? (хотя меня немного коробит просто от пафоса этого словосочетания: "мощность системы типов", если честно, но я вынужден это словосочетание повторять, поскольку эта мощь почему то оказывается главным критерием оценки языка, неожиданно для меня).
Там же все это уже можно в последних стандартах (тайпклассы, Comparable, ...)
Есть разное "можно". Например, как нам хорошо известно, в любой параметрический полиморфизм "классы типов" можно (легко и просто) добавить через препроцессинг. Но есть разница (с) с тем, когда классы типов это часть системы типов :-)
хотя меня немного коробит просто от пафоса этого словосочетания: "мощность системы типов"
Зря. Лямбда-куб - как наглядная демонстрация той самой "мощности системы типов" - существует уже лет тридцать как.
Мощность системы типов не является главным критерием оценки языка. Языки со статической типизацией фактически реализуют подход, что код должен быть написан два раза: поведенческий, работающий со значениями, и декларативные аннотации типов, а язык сверит эти два варианта кода. Чем мощнее система типов, тем больше времени вы проведёте в борьбе с ней, она будет заставлять вас переделывать поведенческую часть.
Считайте, что мощность системы типов обратно пропорциональна количеству необходимых приведений типов (static_cast/reinterpret_cast/dynamic_cast)
Там же все это уже можно в последних стандартах (тайпклассы, Comparable, ...), как там оценивается мощность системы типов?
Там нет тайпклассов, концепты не позволяют universal quantification, концепты не могут быть объектами высшего класса, и тела функций не проверяются согласно концептам.
Зависимых типов тем более нет и никогда не будет.
Короче, в плюсах всё очень плохо что с точки зрения выразительной силы, что с точки зрения эргономики того, что есть (хиндли-милнера тоже не завезли, скажем).
Всё, на этом возможности системы типов в Java заканчиваются, вы не можете декларативно добавить каких-то ограничений и статически их проверить.
Ну не всё так печально, конечно. В 8-ке появляются лямбды, и "типа" функциональные типы. Что-то получается через них провернуть. В Java 17 появляются "замкнутые" (sealed) иерархии и - соответственно - какие-то "зайчатки" ADT.
Но это всё всегда упирается в фундамент древней (что первично) и "фанатично" номинативной (что вторично, и - по факту - просто следствие древности) системы типов.
Например, нельзя "просто так" добавить в систему типов bottom type, которого там нет в силу "древности". Точнее он там таки есть, но "другой" :-) А без него, и само по себе "печально"... да и какой-нибудь "вывод типов" работает, мягко говоря, "порой странновато".
Вот вы все тянете в свою сторону - в сторону того чтобы было что-то НЕЛЬЗЯ.
Я хочу, чтобы нельзя было писать не соответствующие спеке программы. И чем более развита система типов, тем точнее я могу выразить спеку.
В плюсах я могу написать функцию
template<typename T>
std::vector<T> sort(std::vector<T>, Comparator<T>);но у меня нет способов композабельно и проверяемо компилятором выразить, что результирующий массив действительно отсортирован. Эта функция так может возвращать исходный массив без какой-либо сортировки, или пустой массив, или сделать partial sort на первых 20 элементах.
В языках с очень развитой системой типов я могу написать
sort : (input : List a)
→ (ord : Order a)
→ (res : List a ** Sorted res ord input)— функция возвращает и отсортированный список, и доказательство того, что этот список — отсортированная версия входа. И отмечу, что в так часто вспоминаемом в таких тредах хаскеле так нельзя, его система типов для этого недостаточно развита.
А мне надо чтобы было МОЖНО!
А почему б не писать на джаваскрипте? Там вообще всё можно, никаких запретов!
Например, чтобы выразить, что строку с числом сложить можно.
Ну пишете
addNumStr : String → Int → Stringи можете сложить строку с числом даже в языке со строгой и развитой системой типов.
А в языке с очень развитой системой типов можно заодно доказать, скажем, что
s1 + addNumStr s2 n = addNumStr (s1 + s2) n
addNumStr "" n = toString nи прочие угодные вашей душе вещи.
Получается что у вас обратная парадигма - все ИЗНАЧАЛЬНО можно (пусть и с непонятными результатами)
Нет, не получается.
Если конечно у меня есть право тоже что-то объяснять.
Это право есть у всех, кто сначала разобрался в предмете.
Это право есть у всех, кто сначала разобрался в предмете.
Значит в этом вашем предмете запрещено разбираться.
Вы разве не знаете что попытка объяснить (для себя и попросить прокомментировать объяснение знатоков или тех кто таковыми себя считает) это и есть попытка разобраться?
Выходит у вас закрытая каста, извините что потревожил.
Афтору честь и хвала, но я считаю, что использовать подобные способы написания функций в java - это выстрел себе в ногу. Интересно, у кого-то был опыт чтения своего ТАКОГО же кода на следующий день? Это же неподдерживаемое месиво, имхо.
Поддерживаю ораторов выше: не очень понятно, зачем забивать шуруп молотком (хоть он и может держаться лучше, чем завинченный отвёрткой гвоздь). Я придерживаюсь мнения, что каждый язык программировнаия лучше использовать в рамках его идеологии. Хочется ФП – лучше выучить какой-нибудь язык ФП.
Большое спасибо за статью!
Получился действительно достаточно простой способ объяснить относительно не простые для понимания концепции.
Монады на Java