Комментарии 265
Из прочитанного понял только одно: в функциональщине хреново организована работа с кортежами. И все десять девять пунктов спекулируют на этом факте.
P.S.
Сам я не функциональщик, и возможно слабо понимаю предмет.
Всё, что пробовали — так или иначе переложение на уровень ниже существующей програмной эмуляции.
У hardware есть состояние, и это хорошо мапится на императивщину.
А эффективно склонировать большой объём данных, изменив где-то во всей структуре 1 поле, это проблема.
Опять же, есть DSL на Haskell для программирования FPGA.
При желании, замыкания можно сделать через редукцию графов, а рекурсию — через Y-комбинатор.
Но что делать, если программа генерирует много замыканий, которые отличаются друг от друга ленивой функцией, а позже их вызывает (например, на действия пользователя). Как это положить на FPGA?
Взять конкретную инженерную задачу — лисп-машина. Как её будет инженер решать на FPGA? Скорее всего, сделает обычный фон-неймановский процессор, на котором будет крутиться интерпретатор. Сделает програмный или аппаратный менеджер памяти, чтобы учитывать занятые и свободные cons-ы и мапить их на непрерывный массив адресов.
У вас есть другие идеи?
в FPGA максимальная возможная рабочая частота зависит от архитектуры. Иногда требуемая архитектура может просто не развестись. Это такая же пакость как сама рекурсия, которая упирается в глубину стека, которая конечна и непредсказуема.
В итоге если пытаться реализовывать функциональное программирование в FPGA, то кажущееся ускорение от одновременного вычисления результатов функций будет нивелироваться общим понижением тактовой частоты.
FPGA хороши для комбинаторной логики и небольших FSM (шифраторы/дешифраторы/кодировщики/конвертеры, в общем та область, где FPGA и ASIC и используются). Если логика по сути подразумевает развесистый конечный автомат (читай императивный алгоритм со сменой состояний), то не имеет никакого значения в прокрустово ложе каких пуристских абстракций вы будете пытаться это уместить — в итоге это отъест так много скорости (засчёт конвейеризации) и ресурсов (на хранение состояния, а точнее на реализацию разных возможных ветвей исполнения), что быстрее это работать не будет, если вообще будет реализуемо.
В итоге сначала начали реализовывать программно обычные процессорные ядра прямо в FPGA, а сейчас вовсю тренд на гибриды на одном кристалле ARM и FPGA.
По сути HDL, на которых ведётся разработка для ПЛИС — это как C с точки зрения Ada, но не как LISP или Haskell. На самом деле тут приходится думать о низком уровне и заниматься оптимизацией больше, чем при программировании на Си для компьютера.
0. а какой должна быть архитектура компьютера для функционального кода?
1. и чем не годится преобразование LISP-style кода f(a,b) в Forth-Style код a b f с последующей компиляцией?
Функциональные языки позволяют записать многие вещи крайне элегантно. Для меня функциональный подход довольно близок, так как он является одной из парадигм, реализованных в Wolfram Language. И как показывает практика, да, математикам, конечно, писать на них проще и даже естественно.
И эта проблема не является эксклюзивной для функциональных языков. Каждый раз, когда мы поднимаемся на более высокий уровень абстракции, мы теряем в производительности. Но если при этом растет скорость и простота разработки, и вообще появляется хоть какое-то преимущество, всегда найдется сфера применения, где это преимущество окажется важнее недостатков. Вопрос только в том, насколько эта сфера применения будет широка.
И вот исходя из этого и следует определять, где ФП применимо, а где нет. В последнее время просто была тенденция, форсить функциональные языки везде и всюду, как какую-то новую религию. А потом уже и в комментариях на Хабре, на прочих форумах, и вообще в разговорах с другими программистами, я стал замечать, что некоторые видят написание чего-либо в функциональном стиле, как самоцель. Будто если программа написана на функциональном языке, то это ее главное преимущество. Поэтому появление таких вот контр аргументирующих статей вполне закономерно. Такое и с другими новыми технологиями/языками случается. Сначала появляется шквал статей, где какая-то технология описывается, как серебряная пуля. Следом идут статьи, где происходит срыв покровов, и объявляется, что серебро оказалось простой нержавейкой.
Программисты в своей массе вообще довольно «религиозны». Я конечно же имею в виду не традиционные религии, а фанатичное следование каким-либо трендам. Для многих ООП — это настоящая религия. А для других, вот — ФП. Есть те, для кого нет бога кроме MVC. А кто-то делает идола из соглашений по написанию кода. Встречались мне и более экзотические верования, например священный джихад против множественных return'ов из функции. Может и в других профессиях такое наблюдается, но мне почему-то кажется, что программисты, чаще всего участвуют в священных войнах. Проблема еще в том, что у нас это легко может выйти за пределы какого-нибудь форума. У многих есть возможность проталкивать свои идеи на практике. И вот тут надо проявлять осторожность.
Кроме того, как раз тогда, когда на глаза попались эти статьи, я узнал, что в одном университете на специальной программе обучения ИТ для людей с не-ИТ образованием (типа биологов или химиков) обучение начинают с диалекта Лиспа. И лично мне показалось, что это уже слишком и надо что-то с этим делать. Вот, к примеру, как-то компенсировать бесконечный поток предвзятых и, в общем, необоснованных похвал и агитации за функциональные языки.
я узнал, что в одном университете на специальной программе обучения ИТ для людей с не-ИТ образованием (типа биологов или химиков) обучение начинают с диалекта Лиспа. И лично мне показалось, что это уже слишком и надо что-то с этим делать.Помню, читал в комментах к прошлой статье, как знакомая автора коммента, уехавшая в Германию, жаловалась ему что их там бедных Лиспу учат и как это ретроградно и ужасно, прямо ах!.. А я почему-то сильно подозреваю, что речь идет о том самом MIT-овском курсе SICP на Scheme, о котором существует мнение (и я его разделяю), что это введение в специальность программиста как таковую. А «что-то с этим делать» совершенно не обязательно — деградация поколения селфи уже происходит и без вашей помощи.
https://ru.wikipedia.org/wiki/LISP
Функциональная парадигма является для Лиспа «родной», поскольку основой архитектуры его является лямбда-исчисление Чёрча. Собственно, именно с Лиспа началось функциональное программирование как практическая методология разработки программного обеспечения.
В Lisp функционального не больше чем в плюсах.
В стандартах C++ ничего функционального не припомню => C++ не ФП
=> Вы утверждаете, что LISP тоже не ФП.
Вы дернули цитату из википедии из раздела «Парадигмы программирования в Лиспе», что вам помешало прочитать остальные пункты, кроме ФП, я не знаю.
Вы генерируете мои утверждения и сами их опровергаете. Мне кажется я лишнее звено в вашем самсобойчике.
В Lisp функционального не больше чем в плюсах.
вот пруф https://habrahabr.ru/post/303984/#comment_9674488
Если в C++ нет ФП => получается, что Вы утверждаете, что в LISP нет ФП,
PS Про ООП и ИП — в Вашем отверждении ничего нет вообще, я его процитировал полностью как есть.
Вас лиспер в детстве обидел? :)
От чего Ваше утверждение «В Lisp функционального не больше чем в плюсах.» меня оскорбила до глубины души.
C++ сожержит чисто функциональный язык шаблонов. В CL язык макросов совпадает с основнам языком со всеми его императивными возможностями, то есть здесь плюсы даже более функциональны :-).
И Basic и Java — тоже критикуют за медлительность, но никто из тех кто пишут на ИП-языках не утверждает про них «это не ИП».
Текущая статья показалась интересной и глубокой технически.
У ФП, как и везде, полно проблем, которые конечно стоит обсуждать. Но в данной статье не наблюдается ничего кроме громких заголовков и невнятного бормотания в качестве аргументации. Автор говорит, что вокруг ФП полно мифов, но сам льет воду на мельницу нескольких расхожих мифов:
1. Код в функциональном стиле медленне, чем императивный
2. Простые задачи (типа квиксорт) в ФП решаются неадекватно сложным способом
3. Функциональный код пишут только высокомерные математики
В реальной жизни, как всегда, это все несовсем так:
1. Быстродействие программ зависит в первую очередь от архитектуры, дизайна и грамотного применения библиотечных средств (особенно — коллекций), и в последнюю очередь — от выбранной парадигмы
2. Вряд ли такое будет часто, но если вдруг видите, что функционально решение получается слишком сложным — просто перепишите этот кусок в императивном стиле, или тут прям одни Haskell-программисты сидят?
3. Люди бывают разные, делать по отдельныи индивидам глубокие выводы о всем ФП-сообществе неправильно
Вряд ли такое будет часто, но если вдруг видите, что функционально решение получается слишком сложным — просто перепишите этот кусок в императивном стиле, или тут прям одни Haskell-программисты сидят?А зачем тогда вообще начинать писать на функциональном языке, если можно сразу начать на императивном и избавиться от всех проблем?
А зачем тогда вообще начинать писать на функциональном языке, если можно сразу начать на императивном и избавиться от всех проблем?
Потому что в «чистых» императивных языках тоже полно всяких проблем. И если решето Эратосфена я писал один раз в жизни как тестовую задачу, а weak hash table использовался всего в двух проектах, из тех, где я участвовал, то обработку коллекций (фильтрация, маппинг, свертка) я пишу по много раз в день, и в императивном виде это просто адски неудобно. Надежный параллелизм реализовать сложно, моки для юнит-тестов иногда выглядят страшнее, чем код, который они тестируют. Люди переходят на ФП не от хорошей жизни, а спасаясь от ошибок, связанных с изменяемым состоянием и побочными эффектами.
А зачем тогда вообще начинать писать на функциональном языке, если можно сразу начать на императивном и избавиться от всех проблем?
Если бы в императивных языках не было проблем, то сейчас бы мы о функциональных не дискутировали. У функциональных свои плюсы и минусы, у императивных — свои. Мультипарадигменные пытаются совместить плюсы и избавиться от минусов, хотя бы простотой перехода от одной парадигмы к другой в рамках не то что одного проекта, а в рамках одного блока кода. Если вы когда-нибудь писали код типа
var activeItem = items.filter((item) => item.isActive)
, то вы совмещали преимущества обоих подходов, переходя от императивной парадигмы к функциональной в рамках одной строки кода.Если это ФП, тогда вот это тоже ФП:
int compareLen(void* p1, void* p2) { return ((Item*)p1)->len - ((Item*)p2)->len; }
qsort(items, N, compareLen);
Быстродействие программ зависит в первую очередь от архитектуры, дизайна и грамотного применения библиотечных средств (особенно — коллекций), и в последнюю очередь — от выбранной парадигмы
Не убедительно:
0. быстрота на практике часто не требуется (а важно другое, например, ясность кода и т.п.)
1. если требуется именно быстрота, то пишут на C, или смеси C и C++
PS чем не годится преобразование LISP-style кода f(a,b) в Forth-Style код a b f с последующей компиляцией?
Хотелось бы услышать подробно аргументированную критику.
1. если требуется именно быстрота, то пишут на C, или смеси C и C++
Если требуется быстрота, включают голову, корректируют архитектуру и дизайн, включают профайлер и оптимизируют критические места в коде. Да-да, именно так, причем независимо от ЯП. Никакая волшебная смесь С и С++ не поможет написать быстрый код в отсутствие мозгов. А при их наличии, можно писать достаточно быстрый код практически на чем угодно.
PS Что за привычка сначала судить об эффективности по скорости, а затем пытаться натянуть сову на глобус?!
чем не годится преобразование LISP-style кода f(a,b) в Forth-Style код a b f с последующей компиляцией?я так от Вас и не услышу?
Хотелось бы услышать подробно аргументированную критику.
(defun make-adder (n)
(lambda (x) (+ n x)))
(setq my-add 20)
(setq my-func (make-adder my-add))
(my-func 10)
8. Понадобилось 50 лет, чтоб разбавить концентрацию высокомерных снобов в сообществе до той степени, чтоб можно было получить полезный ответ по функциональному программированию
По моему это единственная стоящая фраза.
(Я не говорю что остальные не релевантные, но они точно не новые.)
Бросается в глаза желтизна некоторых пунктов:
3. Не существует чисто функциональных потокобезопасных коллекций
И ниже поясняется, что речь идет об изменяемых коллекциях, которых в чисто функциональных языках не может быть по определению, безотносительно потокобезопасности.
6. Как оказывается, все существующие реализации функциональных языков спроектированы так, что аллоцируют слишком много памяти
Как оказывается, все существующие высокоуровневые языки спроектированы так, что аллоцируют слишком много памяти — и все сразу побежали писать на ассемблере.
7. Чистое функциональное программирование хорошо для параллелизма в теории, но не очень хорошо на практике, а высокая производительность на практике—единственная настоящая задача параллелизма
Непонятная формулировка и сумбурное пояснение. В ФП ряд неприятных проблем, возникающие при параллелизме, исключены как класс. В чем недостаток-то?
Последние 2 пункта больше ad hominem, чем объективные недостатки ФП как подхода.
Непонятная формулировка и сумбурное пояснениеО, это я пропустил фразу в заголовке, спасибо. Теперь он вот такой: «Чистое функциональное программирование хорошо для параллелизма в теории, но не очень хорошо с точки зрения производительности на практике, а высокая производительность на практике—единственная настоящая задача параллелизма»
В ФП ряд неприятных проблем, возникающие при параллелизме, исключены как класс. В чем недостаток-то?Как следует из текста, а теперь и из заголовка, в низкой производительности.
а высокая производительность на практике—единственная настоящая задача параллелизма
Смотря что означает «высокая». Для многих не особо важны абсолютные цифры на конкретной конфигурации, но важна практически неограниченная возможность горизонтального масштабирования.
Как следует из текста, а теперь и из заголовка, в низкой производительности.
Низкая производительность — понятно. Но причем тогда параллелизм? До сих пор не понимаю смысл этого пункта. Особенно: «остерегайтесь людей, которые говорят о масштабируемости программ без учета абсолютной производительности» — что такое «абсолютная производительность», о чем здесь вообще речь?
И если ваша однопоточная программа на С работает в 1000 раз быстрее, чем однопоточная функциональная программа
Не вижу логики. Если заменить «функциональная программа» на «другая императивная программа на С», арифметика как-то изменится? Если нет — причем тут ФП, в чем конкретно его недостаток? Или в императивных программах не может быть проблем с производительностью?
Я умею работать с массивами, даже знаю пару алгоритмов на базе массивов. Где тут в вашем ФП массивы? Как нет? А что есть? А, списки… в моём любимом языке класс массива называется ArrayList, значит список — это типа массив такой. Сейчас как напрограммирую… Ой, а почему всё так тормозит? Как это нельзя к списку по индексу обращаться, вот же я обращаюсь и всё работает, тупит правда, но это потому что ФП медленный.
Функциональные языки предназначены для работы с неизменяемым состоянием
А есть такой класс задач «работы с неизменяемым состоянием»? Интересно было бы узнать что туда относится?
Но на различных уровнях абстракции неизменяемое состояние сплошь и рядом. Любая чистая функция, то есть функция, возвращаемый результат которой зависит исключительно от аргументов и аргументы она не меняет, на уровне клиента этой функции есть функция, не изменяющая состояние клиента. Да даже не функция, а банальное выражение a + b является выражением, не изменяющим состояние клиента. Пока он сам не решит изменить своё состояние, выполнив присваивание c = a + b. Грубо, к классу задач «работы с неизменяемым состоянием» относятся все задачи, которые генерируют новые значения на основании существующих, не изменяя существующих, все задачи в алгоритмах которых можно обойтись без переменных, без операций «присвоить» и «изменить», только «вернуть результат выражения».
А есть такой класс задач «работы с неизменяемым состоянием»? Интересно было бы узнать что туда относится?
Задача — это что надо сделать. Использовать неизменяемое состояние — это то, как делать. Функциональные языки являются тьюринг-полными, поэтому на них можно решить ровно такое же множество задач, что и на императивных. Однако эффективность таких решений может быть разная.
Об этом уже шел разговор в этой ветке. Если коротко резюмировать сказанное там, я считаю что функциональный подход надо использовать по умолчанию для любых прикладных (не системных) задач, поскольку функциональные программы в среднем выигрывают по корректности и простоте и незначительно уступают по быстродействию.
Если вы из тех, кто думает, что неизменяемых данных не существует в природе, попробуйте прочитать этот комментарий, а еще лучше — самостоятельно написать что-нибудь в функциональном стиле, и очень скоро ваше мнение изменится.
Императивная программа хорошо работает на конкретной среде выполнения традиционного компьютера, под который была написана. При этом работает максимально эффективно. Но иногда среда выполнения меняется или, более того, требуется чтобы пользователь сам ее задавал на свое усмотрение. Тогда приходит функциональное или логическое программирование. Мы просто повышаем уровень абстракции.
Однажды я предложил нескольким программистам на Хаскеле (и у некоторых из них были диссертации по этому языку) написать на нем эффективную параллельную быструю сортировку (quicksort)—и вот что из этого вышло.С большим нетерпением жду перевода.
This solution does run correctly on small inputs but increasing the problem size to 1,000,000 elements results in a stack overflow. Two attempts were made to diagnose this bug, here and here, but both turned out to be wrong. The bug is actually in the getElems function of the Haskell standard library which stack overflows on long arrays.
Surprisingly, more bug fixing seems to have culminated in the world's first parallel generic quicksort written in Haskell. Furthermore, the resulting Haskell solution is only around 55% slower than the equivalent F# solution. Note that this requires the latest GHC that was only released in recent weeks.
И, кстати, корректно ли сравнивать ФП и ООП, и называть последнее императивщиной?
Мне кажется, ООП — более высокий уровень абстракции, и оно может существовать и с ФП «под капотом» (как в Scala и OCaml).
>Мне кажется, ООП — более высокий уровень абстракции
Уровень абстракции у ООП высок на этапе описания предметной области в виде структуры классов. Это, можно сказать, декларативная часть ООП. Описание классов и отношений между ними — декларативно, А код методов императивен. И работа с экземплярами классов происходит в императивном режиме. То есть сочетается сразу два уровня абстракции. Чем лучше мы отразим предметную область в терминах классов, тем более высокоуровневым будет наш код. То есть мы можем варьировать уровень абстракции. Возможно именно поэтому ООП пользуется таким успехом. Но, конечно же, у этого варьирования есть свои пределы. Именно поэтому, код в ОО стиле кажется избыточным в очень маленьких проектах — слишком много лишней декларативщины. И поэтому же, код становится слишком запутанным на очень больших проектах — мы не можем избежать императивного кода, а на большом проекте, накапливается его критическая масса. То есть абстракции, предоставляемой ОО подходом становится недостаточно. В этом плане функциональные элементы хорошо дополняют ОО язык, позволяя уменьшить количество императивного кода в некоторых местах. А те же лямбды помогают не засорять декларативную часть всякими служебными, одноразовыми методами, не имеющими никакого смысла в терминах предметной области. Но эти функциональные элементы не имеют особого отношения к чистому функциональному программированию. Ведь предметная область по прежнему остается описана в терминах классов, а основная часть логики по прежнему описывается императивно.
Но опять же, надо учитывать, что большинство применяемых ОО-языков не являются строго ОО-языками. Они все мультипарадигменные. Так уж получилось, что ОО-парадигма хорошо совмещается с элементами других парадигм. Наверное это вторая причина успешности ООП. Если подумать, из всех ОО-языков, которыми я хоть как-то владею, более-менее консервативным, в плане поддерживаемых парадигм, был только Delphi. При чем именно старый Delphi, в котором даже Generic'ов не было. И что-то я не заметил, чтобы он всем очень нравился. Так что, сколько бы не говорили о засилье ООП, на самом деле, самой популярной парадигмой была и остается мультипарадигменность. Потому что ни одна парадигма в отдельности не является достаточно универсальной, чтобы на ней было удобно делать абсолютно всё.
Запутанный код в больших проектах, он запутанный относительно чего?
Как вам, кстати, относительно простые фп-программы, наподобие вышеприведенных «настоящих» квиксортов на хаскелле? Не кажутся запутанными? По сравнению с сишным-то аналогом?
Вам сугубо ооп-шные анонимные классы в java «лямбдами на стероидах» не кажутся? Да, синтаксис более громоздкий, но в целом-то?
Мне вот всегда казалось, что фп — это такая до смешного пафосная альтернатива для любителей процедурного программирования, не осиливших то же промышленное ооп. Наверное, это очень сильно сказано, но таково мое мнение.
Мне вот всегда казалось, что фп — это такая до смешного пафосная альтернатива для любителей процедурного программирования, не осиливших то же промышелнное ооп. Наверное, это очень сильно сказано, но таково мое мнение.
Ни в коей мере не посягая ни на столь глубокомысленное мнение, ни на ООП в целом, хочу заметить, что все-таки ФП и ООП — это очень разные вещи. Сферы их применения, как мне кажется, пересекаются только в части организации модульности и повторного использования кода. ФП говорит о «чистых» функциях и функциональной композиции, а ООП — о наследовании, инкапсуляции и полиморфизме.
ООП как Библия — есть конечное число канонических текстов (гоф, рефакторинг, чистый код и т.п.), но понимают их все по-разному. Одну и ту же задачу сто разных программистов «осиливших промышленное ООП» задизайнят по-разному. И все эти сто дизайнов будут неправильными и подлежащими рефакторингу с точки зрения сто первого программиста, который реально разбирается в такого рода задачах.
ФП предъявляет простые формальные требования, проверить которые может любой — это неизменяемость состояния и отсутствие побочных эффектов. Требования соблюдены — значит дизайн правильный. Если видение задачи изменилось — можно поменять отдельные функции в композиции или порядок их вызова. Подход гораздо проще.
ООП — о наследовании, инкапсуляции и полиморфизме.Есть мнение, что это очень искаженное представление об ООП.
«OOP means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things» © A.Kay
Поэтому практически значимые отличия ООП и ФП:
1) (Им-)мутабельность данных. В ФП данные неизменяемы, что затрудняет реализацию некоторых привычных алгоритмов, основанных на in-place модификации данных. Зато упрощает сборку мусора и конкурентный доступ.
2) Работа с разделяемым состоянием. ООП равномерно размазывает его по всей программе, попутно позорно нарушая инкапсуляцию при помощи геттеров и сеттеров. А ФП изолирует работу с разделяемым состоянием. И вместо блокировок использует очереди сообщений или STM.
Вот это, лично мне, кажется наиболее спорным моментом в ФП. Дело даже не в затруднении реализации привычных алгоритмов, а в том, что при добавлении элемента в список, мне нужно создать целый новый список. А если он большой? Это мне кажется настолько чудовищно неоптимальным, что все остальные плюсы просто меркнут на этом фоне. На бумаге концепция может и хороша, но мы то на компьютере работаем. И память у него не бесконечная. Да и аллоцируется она не мгновенно. Такое поведение выглядит просто как неуважение к вычислительной технике: «Вообще плевать на ее особенности, пусть пашет на износ, у нас тут всё иммутабельно». Ну нельзя же так.
Сравнить хотя бы два этих куска кода:
//1-------------
string sResult = "";
DateTime sBegin = DateTime.Now;
for(int i = 0; i < 200000; ++i){
sResult += "TEST" + "_";
}
DateTime sEnd = DateTime.Now;
Console.WriteLine(sEnd - sBegin);
//2-------------
DateTime sbBegin = DateTime.Now;
StringBuilder sbResult = new StringBuilder();
for(int i = 0; i < 2000000; ++i){
sbResult.Append("TEST");
sbResult.Append("_");
}
DateTime sbEnd = DateTime.Now;
Console.WriteLine(sbEnd - sbBegin);
Обратите внимание, для StringBuilder я использовал не двести тысяч, а два миллиона итераций. Потому что на двухстах тысячах он выполняется вообще мгновенно. А на двух миллионах за пол-секунды.
А вот строку не советую проверять больше чем на двухстах тысячах. Она и на них выполняется целых полторы минуты.
Разница даже не в разы, а на несколько порядков. Зато строка иммутабельна.
Я не спорю, что какие-то вещи можно неплохо оптимизировать, особенно такие простые, как вставка в конец списка. Но как-то вот смотрю на иммутабельные строки, и они мне не нравятся. В шарпе у меня хотя бы есть неиммутабельный StringBuilder. А как выкручиваться, если всё вокруг будет иммутабельно, я лично не знаю.
З.Ы. Возможно я действительно чего-то не понимаю, просто я никогда не писал именно на строго функциональных языках. Хотя активно использую, и рекурсии, и замыкания. А уж про лямбды и упоминать смысла нет. Но вот именно с иммутабельностью у меня как-то не заладилось. Возможно все дело в том, что в нефункциональных языках она не оптимизирована. Непонятно только, зачем тогда ее вообще вводить, без оптимизации-то. Может расчет как раз на то, что в случае необходимости можно использовать неиммутабельный аналог?
Сейчас кстати посмотрел, в F# есть не только иммутабельные списки, но и мутабельные. То есть никакого фанатизма. Пожалуй изучу F# для общего развития.
Еще нашел статью, где человек как раз рассказывает про проблему с иммутабельными списками в F#. Как я и предполагал, там проблема с тем, что тратится много времени на аллокацию и сборку мусора:
Берегитесь иммутабльных списков в F# при параллельной обработке
Статья на английском. Если вкратце, то он заменил иммутабельные списки, мутабельными массивами, и вручную следил за иммутабельностью. Выигрыш в производительности получился в 20 раз.
Что самое интересное, он пишет, что именно иммутабельные списки на практике приводили к проблемам с синхронизацией в параллельной обработке. Хотя казалось бы, иммутабельность должна упрощать параллелизм.
Которые не еквивалентны.
Вы забыли таки построить строку, которую делаете билдером. Сейчас у вас измеряются аллокации/копированя для новых строк (вариант 1) и добавление элемента в конец списка (вариант 2).
static void Main ( string [ ] args )
{
Console.WriteLine ( "Test string speed." );
Console.WriteLine ( "Input string part:" );
string part = Console.ReadLine ( );
Console.WriteLine ( "Input delimeter:" );
string delim = Console.ReadLine ( );
//string
Console.WriteLine ( "Input count for string:" );
int sCount = 0;
try
{
sCount = Int32.Parse ( Console.ReadLine ( ) );
}
catch { }
string result = "";
DateTime sBegin = DateTime.Now;
Console.WriteLine ( "Test string start with "+sCount+" iteration... ["+sBegin+"]" );
for (int i = 0; i<sCount; ++i )
{
result += part + delim;
}
DateTime sEnd = DateTime.Now;
Console.WriteLine ( "Test string end. "+sCount+" iteration passed. [" + sEnd + "]" );
Console.WriteLine ( "Result string length:" );
Console.WriteLine ( result.Length );
Console.WriteLine ( "Time Passed:" + ( sEnd - sBegin ) );
//StringBuilder
Console.WriteLine ( "Input count for builder. Input \"0\" if you want to keep current count("+sCount+"):" );
int bCount = 0;
try
{
bCount = Int32.Parse ( Console.ReadLine ( ) );
}
catch { }
if ( bCount == 0 ) bCount = sCount;
string bResult = "";
StringBuilder builder = new StringBuilder();
DateTime bBegin = DateTime.Now;
Console.WriteLine ( "Test StringBuilder start with " + bCount + " iteration... [" + bBegin+"]" );
for ( int i = 0; i < bCount; ++i )
{
builder.Append ( part );
builder.Append ( delim );
}
bResult = builder.ToString ( );
DateTime bEnd = DateTime.Now;
Console.WriteLine ( "Test StringBuilder end. " + bCount + " iteration passed. [" + bEnd + "]" );
Console.WriteLine ( "Builder Result length:" );
Console.WriteLine ( bResult.Length );
Console.WriteLine ( "Time Passed:" + ( bEnd - bBegin ) );
//Result
Console.WriteLine ( );
Console.WriteLine ( "-------------------------------------------------" );
Console.WriteLine ( );
Console.WriteLine ( "Simple string time:" + ( sEnd - sBegin ) + "with " + sCount + " of iterations." );
Console.WriteLine ( "StringBuilder time:" + ( bEnd - bBegin ) + "with " + bCount + " of iterations." );
Console.WriteLine ( );
Console.WriteLine ( "Press any key" );
Console.ReadKey ( );
}
На тему массивы vs списки — это ж вообще на уровне К.О. Если не понимать разницу между этими структурами данных, то очень легко писать тормозной код — это факт. По сути все проблемы от того, что некоторые программисты пытаются в силу инертности мышления работать с неизменяемыми структурами данных так, как-будто они изменяемые. Но из этого ничего хорошего не получится и как-то глупо в этом ФП обвинять, оно в инертности мышления не виновато )))
P.S. К.О. просит передать: Нельзя на базе списков реализовывать алгоритмы, предусматривающие доступ к элементам по индексу.
А с точки зрения логики, если бы реализация этих структур принципиально не отличалась, то достаточно было бы одного названия )
Возможно название класса StringBuilder несколько сбивает с толку. Я не знаю почему Microsoft его так назвали, но это не «построитель строк», это именно строка, которую можно свободно модифицировать. В частности вставку можно делать в произвольное место:
string s = Console.ReadLine();
StringBuilder sb = new StringBuilder();
sb.Append ( s );
sb.Insert ( s.Length / 2, Console.ReadLine ( ) );
Console.WriteLine ( sb.ToString ( ) );
Внутри это не список строк, а список массивов char. Один такой массив может содержать символы из нескольких строк, добавленных Append'ом, в то же время, одна строка может размазаться на несколько массивов. Причем, как массивы, так и весь список — мутабельные. В частности, методы Append, Insert, Remove и т.д. возвращают не новый StringBuilder, а this. Так же, будь он иммутабельным, наверняка бы реализовывал интерфейс IClonable, т.к. было бы достаточно вернуть this. Собственно в классе String так и сделано. И вообще, если бы StringBuilder был иммутабельным, в нем бы не было смысла, т.к. он бы не отличался от обычного string'а. В MSDN есть ссылка на исходник (Reference Source), так что там все можно конкретно посмотреть.
private void MakeRoom(int index, int count, out StringBuilder chunk, out int indexInChunk, bool doneMoveFollowingChars)
{
...
chunk = this;
while (chunk.m_ChunkOffset > index)
{
chunk.m_ChunkOffset += count;
chunk = chunk.m_ChunkPrevious;
}
indexInChunk = index - chunk.m_ChunkOffset;
// Cool, we have some space in this block, and you don't have to copy much to get it, go ahead
// and use it. This happens typically when you repeatedly insert small strings at a spot
// (typically the absolute front) of the buffer.
if (!doneMoveFollowingChars && chunk.m_ChunkLength <= DefaultCapacity * 2 && chunk.m_ChunkChars.Length - chunk.m_ChunkLength >= count)
{
for (int i = chunk.m_ChunkLength; i > indexInChunk; )
{
--i;
chunk.m_ChunkChars[i + count] = chunk.m_ChunkChars[i];
}
chunk.m_ChunkLength += count;
return;
}
...
}
Грубо говоря, если императивная программа делая в цикле
x:=x+1
, полностью помещается в регистры процессора и миллион итераций для неё ничто, то некорректно написанная хаскель-программа может гонять огромный memory-трафик, конструируя цепочки выражений x0+1+1+1+...+1.Зато, если если результат не пригодится, можно не выполнять сложения. Толку от этого, если выполнить честное сложение на порядки быстрее, чем разводить такие деревья в памяти.
Пример утрирован, но общий смысл — программист на Хаскель не может быть спокоен за оптимальность своих сложных алгоритмов без постоянного профайлинга, ленивая хитрая машина может подставить в любой момент.
А вот в императивных языках — как написал, так и работает.Вы уверены, что это в императивных языках, а не в Ваших фантазиях? )
Откуда ж берутся null pointer exception, GIL и ещё 100500 подводных камней, когда работает совсем не так, как написал… Проблемы везде есть, и в императивных языках их совсем не мало. Но можно найти проблемы и в Прологе и в Хаскеле, тут никто не спорит. А если пытаться на них в императивном стиле программировать, то вообще феерический ппц будет, о чём и статья.
Если возникла NPE, это определённо ошибка алгоритма, а не эффекты среды выполнения, которые сложно просчитать заранее.
NPE (в случае, когда программер запуталься в коде и что-то не проверил) — это математически некорректный алгоритм.
переполнение стека из-за недооценки глубины рекурсиину вот, ещё одну проблему императивного подхода вспомнили, которая при функциональном подходе отсутствует )))
Программирование — это не математика, поэтому математически корректный алгоритм, который во что-то упёрся, с точки зрения программирования такой же ошибочный, как с NPE.
Хвостовая рекурсия оптимизируется более-менее одинаково, а прочая рекурсия ограничена стеком везде.
Насчёт ограничений и языков… у всех по разному, у Erlang, например, глубина нехвостовой рекурсии ограничена только объёмом RAM.
В функциональном подходе традиционно используется только хвостовая рекурсия
Т.е. традиционно отказываемся от рекурсии, например, на задаче обхода графа в глубину (получения списка файлов в подкаталогах)? Не слышал про такой запрет.
у Erlang, например, глубина нехвостовой рекурсии ограничена только объёмом RAM
Ну а для c++ (x64) стек можно настроить на 100500 терабайт и глубина будет ограничена размером HDD под своп.
Я бы хотел примеров разрушения переданных по ссылке параметров.
Какие грабли оказались для вас самыми больными и неочевидными.
Соглашаясь с теоретической возможностью, в своей практике ничего не могу вспомнить.
(print 10)
Визуально отличен вывод в консоль?
А в ФП-парадигме я могу корректно сказать
x = x.SaveToDB
, или просто x.SaveToDB
и потеряю факт сохранения в базу (потом ищи этот баг), или ещё лучше — у = x.SaveToDB
, у получится 2 объекта: один сохранённый, другой нет, а по факту получу расхождение модели с реальным миром, которое сложнее минимизировать.В момент времени t1 имеем состояние s1, делается шаг (например, в дебагере) — получаем t2 и s2.
Это, всё же, преувеличение. Branch prediction, out of order execution и прочий false sharing, конечно, не замедлят код в миллион раз, но всё же вполне имеют место быть. Как и соответствующие хинты для компилятора (например, restrict, __builtin_expect).
Дело даже не в затруднении реализации привычных алгоритмов, а в том, что при добавлении элемента в список, мне нужно создать целый новый список. А если он большой?На практике это очень легко решается — хвост списка всегда переиспользуется под капотом, он же неизменяемый. Т.е. когда вы добавляете элемент в начало списка — это равнозначно смене указателя на голову списка, никакого копирования данных не происходит.
Это я-то фп-сектант? Я же вам про ООП написал и лишь слегка упомянул функциональные элементы. Я это только так и использую обычно. Ну а то что вода, ну может и вода. Я же в общих чертах писал, без конкретики. Так что можете считать это водой, и даже диванными рассуждениями. Я ведь сидел на диване, когда это писал.
>Запутанный код в больших проектах, он запутанный относительно чего?
Относительно не запутанного кода в маленьких проектах. В коде появляется слишком много взаимосвязей, которые не отражаются диаграммой классов. Тяжело потом разобраться. Но с другой стороны, я не уверен, что можно сделать крупный проект простым. Основная проблема в том, что крупный проект начинается с маленького, и растет постепенно. И изначально заложенная архитектура почти никогда не может предсказать дальнейшего развития. То есть банальная болезнь роста. Опять же, я не утверждаю, что это проблема исключительно ООП. Это проблема людей. А ООП здесь просто не может нам помочь. Но я не думаю, что и ФП поможет. Знал бы, как эту проблему решить, жил бы в Сочи.
>Как вам, кстати, относительно простые фп-программы, наподобие вышеприведенных «настоящих» квиксортов на хаскелле? Не кажутся запутанными? По сравнению с сишным-то аналогом?
Всегда можно найти пример, который легко реализуется одним инструментом, и сложно — другим. Но вообще синтаксис у ФП языков мне не очень нравится. Просто это дело привычки. Мне и Си-шный синтаксис не нравится. Слишком много скобочек. Но это уже вкусовщина.
>Вам сугубо ооп-шные анонимные классы в java «лямбдами на стероидах» не кажутся?
Лямбда это анонимный метод. А анонимный класс — это, как ни странно, анонимный класс. Ну да, у них есть общее, то что они оба анонимные и записываются inline, но это разные сущности. Вообще, если проводить параллели между ФП и ООП, то наиболее близкими сущностями из двух парадигм мне кажутся объекты и замыкания. Близость тут условная конечно, но замыкания имеют состояние, как и объекты.
>Мне вот всегда казалось, что фп — это такая до смешного пафосная альтернатива для любителей процедурного программирования
А ООП это тогда что? Структуры с функциями в качестве полей? А человек это до смешного пафосная альтернатива обезьяне. Так про что угодно можно сказать. Все же критика должна быть конструктивной. Ну можно наверное считать, что функциональное программирование это декларативная форма процедурного программирования. Но что это меняет? Лично мне не кажется, что функциональное программирование намного проще, чем ООП. По мне, так даже сложнее в чем-то. Но каким-нибудь математикам, наверное проще въехать в ФП. И зачастую им больше ничего и не надо для их деятельности, особенно ООП. У них другие задачи. Вы серьезно считаете, что люди, которые пишут диссертации по хаскелю, просто не осилили джаву?
Лямбда это анонимный методНет, это конструкция, пришедшая из лиспа
(lambda (x y) (+ x y))
. Она может захватывать переменные и превращаться в замыкание. В последнем случае она эмулируется анонимным классом, который хранит захваченные переменные. Либо эмулируется анонимной ф-цией, если доп. память не нужна.но замыкания имеют состояние, как и объектыУ замыканий такое же неизменяемое состояние, как и у любых других конструкций ФП.
Одно замечание — языки, в которых замыкания добавлены поверх ООП (c++ и c#) могут захватывать не значение, а ссылку на переменную и тогда покажется, что состояние замыкания можно менять, меняя переменную. На самом деле, захваченные значения (адрес ссылки) не меняются, это побочные эффекты не чистых ФП-языков.
С учетом написанного в статье, сравнивать ФП и ООП с человеком и обезьяной соответственно — провокация покрепче моей, знаете ли! А если уж сравнивать ФП с процедурным, то это как шимпанзе и кошка — шимпанзе (ФП) гораздо выше по своему развитию, да, и может творить просто удивительные вещи, да и человек почти (ну вроде), но только толку-то от его забавных кривляний на практике все равно кот наплакал :)
Ну и про лямбду и классы. С чего вы решили, что если это разные сущности, их нельзя сравнить? И то, и другое содержит некие действия и данные, с которыми будет работать сущность, которой мы их передаем. Только вот объект класса может, например, хранить состояние, и иметь несколько точек входа (методов), а лямбда — лишь одну-единственную точку входа. Там еще есть наследование, интерфейсы и пр., но вы же сами в состоянии про это додумать, правда?
Мне кажется, ООП — более высокий уровень абстракции, и оно может существовать и с ФП «под капотом» (как в Scala и OCaml).
Мне кажется, ФП — более высокий уровень абстракции, и оно может существовать и с ООП «под капотом» (как в Scala и OCaml).
Только вот объект класса может, например, хранить состояние, и иметь несколько точек входа (методов), а лямбда — лишь одну-единственную точку входа.
function createObject(a, b) {
function addAB() {
return a + b;
}
function mulAB() {
return a * b;
}
function aNTimes(n) {
return a * n;
}
return function dispatch(method) {
switch (true) {
case method === 'addAB': return addAB;
case method === 'mulAB': return mulAB;
case method === 'aNTimes': return aNTimes;
default: throw new Error('Method not found');
}
}
}
var o = createObject(4,5);
o('addAB')(); // => 9
o('mulAB')(); // => 20
o('aNTimes')(3); // => 12
Там еще есть наследование, интерфейсы и пр., но вы же сами в состоянии про это додумать, правда?
Но я все-таки надеюсь увидеть пример ООП под капотом ФП. Попробуйте еще.
(Оставлю в стороне все эти срывы покровов, вытаскивания на свет божий преданий старины глубокой и прочую борьбу с ветряными мельницами. Да, все эти тупые джависты, рубисты и прочие недочеловеки просто дураки и не лечатся, и наплевать, что в 99.99% случаев ООП — это-таки наследование-инкапсуляция-полиморфизм, — сейчас мы откроем глаза миру на его вопиющее невежество!!1 Правда, в фпшных скалах и окамлах то же самое почти, вот парадокс.)
Вы передёрнули факты «Мне кажется, ООП — более высокий уровень абстракции, и оно может существовать и с ФП «под капотом»», Вас протроллили в ответ зеркальным передёргиванием «Мне кажется, ФП — более высокий уровень абстракции, и оно может существовать и с ООП «под капотом»».
Теперь Вы скатились до истерики функции vs объекты, что совсем не то же самое, что ФП vs ООП, надеюсь, хоть это очевидный факт.
Ну а истина как всегда где-то посередине, ФП не конфликтует с ООП, они вообще ортогональны и взаимодополняют друг друга.
Зачем вспоминать акторы Scala, я не понял, это ведь частичная калька с Erlang. И да, ООП на базе акторов — это ООП в рамках ФП, которое Вы так жаждали увидеть.
в 99.99% случаев ООП — это-таки наследование-инкапсуляция-полиморфизмну, если Вам легче живётся с этой аффирмацией, то, пожалуйста, никто у Вас её не отнимает
не совсем «ортогональны», если «дополняют друг друга»?Ну, вот была у Вас одна ось X и линейное мышление, а потом Вы добавили ортогональную ось Y, на которой тоже можно линейно мыслить. Но если Вы знаете про обе оси (дополнили одну другой), то у вас уже есть плоскость, а не линия. Так понятнее?
Чем больше парадигм Вы освоите, тем объёмнее станет мышление, а пытаясь кому-то доказать, что одной парадигмы достаточно для всего — Вы добровольно надеваете себе шоры.
Да, хардкорное (да и не-хардкорное) ФП непопулярно, а ООП — это наследование-инкапсуляция-полиморфизм, по статистике. Я не вижу в этом трагедии.
Во-первых «наследование-инкапсуляция-полиморфизм» никогда не было определением ООП. Во-вторых, все бенефиты этой триады не зависят от объектно-ориентированности языка.
Наследование прекрасно заменяется композицией и примесями, что и в рамках классового ООП давно и активно применяется для борьбы с антипаттернами наследования. Инкапсуляция по факту только с иммутабельными данными возможна, пока Вы можете менять аргументы метода по ссылке, можно только притворяться, что есть инкапсуляция. Полиморфизм спокойно реализуется через duck-typing и typespecs (аля интерфейсы), в том числе и во многих ООЯП.
И да, Вы упорно путаете ФП и ФЯП. Поддержка ФП в какой-то мере уже включена почти во все популярные нефунциональные языки программирования. И с каждым годом всё больше языков расширяют эту поддержку… посмотрите хотя бы на C# и Java, на эти оплоты ООП, первый уже давно насквозь пронизан ФП, а второй окончательно признал популярность ФП 2 года назад.
P.S. Ну а на тему quicksort, это алгоритм на базе мутабельного массива. Имхо, совершенно очедичный факт, что он не может быть реализован в функциональном стиле вообще никак. Те эмуляции, которые Вы видели, никакого отношения к реализации quicksort не имеют.
Вы разве не поняли, что я в верхнем посте именно это и имел в виду касательно quicksort?
Но это ничуть не меняет дело, и большинство проектов пишутся в императивно-оопшном стиле.К чему эти обобщения на всю индустрию на основе своего личного опыта? На мой взгляд выбор между ФП и ООП — это совершенно бесполезное словоблудие (аля эта статья) вне контекста конкретной области программирования (а в идеале конкретного проекта). Ни то, ни другое не является лучшим выбором для 100% задач.
Вы разве не поняли, что я в верхнем посте именно это и имел в виду касательно quicksort?Так а в чём смысл обсуждать quicksort в контексте ФП? То, что алгоритмы, основанные на in-place модификации данных, нельзя реализовать в рамках ФП, по-моему, вполне очевидно. Не понимаю, чем это Вас разочаровало…
Я не знаю, зачем вы мне про quicksort рассказываете, причем то, о чем я и сам писал до этого.
В этой статье полно конкретики, а не словоблудия.
Словоблудие — это вот, например: https://habrahabr.ru/post/190492/. Очень прикольненько так, молодежно, мемчиками пересыпать только забыли.
Корректность лучше быстроты. Простота лучше сложности. Ясность лучше хитроумия. Безопасность лучше ненадежности— Г. Саттер, А. Александреску. Стандарты программирования на С++
Благодаря чистоте функций и неизменяемости состояний, в ФП легче доказать корректность решения. Само по себе решение в функциональном стиле выглядит, как правило, проще — за очевидным исключением случаев применения чистого функционального подхода для решения изначально императивных задач.
А при каком применении, по-Вашему мнению, ФП имеет явные преимущества?
В прикладном программировании ФП надо применять по умолчанию:
— если объявляем переменную — делаем ее immutable (если ЯП не позволяет указать это явно, просто берем за правило присваивать значение переменной ровно один раз),
— используем только immutable типы данных (если в библиотеках нет immutable-версии нужного типа, берем за правило создавать новый экземпляр на каждое изменение),
— если пишем функцию — в ней не должно быть побочных эффектов.
И отходить от этих правил только в случае крайней необходимости.
Не во всех современных ЯП удобно и вообще имеет смысл следовать этим правилам, поэтому для применения ФП важна не только задача, но и язык, на котором мы собираемся ее решать. Мне нравится подход Scala, где функциональные стиль поощряется, но не навязывается.
используем только immutable типы данных (если в библиотеках нет immutable-версии нужного типа, берем за правило создавать новый экземпляр на каждое изменение)
И зачем такой откровенный мазохизм? На практике практически все данные — изменяемые. А если нужно посчитать один раз какую-нибудь сложную формулу, то необходимо и достаточно использовать ForTran. А "правило создавать новый экземпляр на каждое изменение" — это вообще пухнущий на глазах песец, который с каждым изменением ЖРЁТ память. С Ваших слов получается, что ФП следует применять вместо ForTran'а, без внятных объяснений почему.
PS не получил ответа на вопрос: при каком применении, по-Вашему мнению, ФП имеет явные преимущества?
Переформулирую вопрос: в каких случаях на практике имеет смысл применять ФП? В каких случаях ФП имеет явное преимущество?
И зачем такой откровенный мазохизм? На практике практически все данные — изменяемые.
Это с философской точки зрения все меняется, и дао подобен реке. А на практике, если внимательно посмотреть на каждую функцию (мы говорим про функциональный подход), окажется, что никакого мазохизма даже близко, в отличие от императивного кода. Входные параметры функции — не меняются в процессе ее работы (во многих императивных языках это тоже стандартная практика). Результат функции получается в процессе обработки входных параметров, но он получается один раз, и не меняется после этого. И даже если результат это какая-то структура, он конструируется весь сразу, одним ударом. Если структура сложная, могут быть промежуточные результаты, из которых уже конструируется конечный, но эти промежуточные результаты также получаются один раз и не меняются после этого.
Есть в императивном программировании такие структуры, с которыми реально удобно работать, именно постоянно их изменяя (коллекции, билдеры). В ФП есть их неизменяемые аналоги, которые при изменении не клонируют все с нуля, а переиспользуют общие данные из предыдущего экземпляра.
Хотя я спрашивал:
А при каком применении, по-Вашему мнению, ФП имеет явные преимущества?, а то что ФП — жрёт память, я уже слышал от противников ФП. Я же хочу услышать от Вас ответ, в каких случаях использование ФП оправданно.
В таком случае вы либо будете использовать готовый ФЯП, либо переизобретёте большую часть ФП на своём любимом ЯП, либо будете страдать на каждом углу от race conditions, dead locks, гейзенбагов и прочих весёлых вещей.
А ФП языки работают на CUDA или OpenCL?
Про программирование на GPU я Вам не подскажу, т.к. не интересуюсь этой темой. Если Вы хотели подколоть на тему, что для параллелизма есть инструменты получше, то вспомнили бы лучше Lambda- и Kappa-архитектуры. А то параллелизм числовых данных на GPU — это насколько узкая тема, что кроме учёных и любителей криптовалют, имхо, никому не интересна.
(просто из того, что данные в ФП считаются неизменяемыми списками, у меня сложилось именно такое представление о применении)
Мне вот интереснее применение ФП для таких проектов как WhatsApp, Twitter и т.д.
берем за правило создавать новый экземпляр на каждое изменение
например, создавать копию гигабайтного массива на каждое изменение элемента по отдельному индексу?
Вы посоветовали ознакомиться с трудом Окасаки «Чисто функциональные структуры данных», где всё популярно расписано. Я выразил мысль, что было бы хорошо, если бы Вы написали об этом статью.
Любая продвинутая СУБД имеет журнал транзакций и режим, когда данные только добавляются.
И можно открутить БД назад к любому состоянию.
В СУБД попроще есть версии записей, одна из которых актуальна, остальные — мусор.
Так что такой подход давно работает с БД.
На самом деле, на уровне физической реализации в современных архитектурах ФП и ИП отличаются лишь точкой зрения, терминологией. Для ИП атомарная инструкция процессора — это команда, изменяющая состояние системы, а для ФП — это функция, применяемая к исходному состоянию системы и возвращающая новое состояние системы с потерей старого.
Это все очевидные вещи, но на мой взгляд, загвоздка тут в самом понятии корректности.
Что такое корректная программа? Это программа, работающая без ошибок. А что такое ошибка в программе? Есть хорошее определение: «В программном обеспечении имеется ошибка, если оно не выполняет того, что пользователю разумно от него ожидать» (Г.Майерс. Надежность программного обеспечения. — М.: Мир, 1980. стр. 12). То есть, в конце концов все упирается в разумные ожидания пользователя. А что делать если требуемая скорость работы явно оговорена пользователем? Или неявно подразумевается, что пользователю разумно ожидать, что какое-то действие программы будет выполняться, скажем минут за 5, а с нашими алгоритмами, оно выполняется за 30? Корректность в данном случае нарушена, т.к. программа не делает того, что от нее требуется. Так что быстротой можно пренебрегать не всегда, и только до определенного предела. То же и с простотой/сложностью. Если простое решение — неправильное, или работает слишком медленно, что как мы выяснили, тоже является неправильным, то придется применять сложное решение. Насчет хитроумия, функциональный код, как раз, выглядит весьма хитроумно. Ну а безопасность, проистекает из корректности. Корректный код и так должен быть безопасным. А программа, не успевающая обрабатывать запросы — ненадежная.
>Благодаря чистоте функций и неизменяемости состояний, в ФП легче доказать корректность решения.
Это верно только для каких-нибудь математических задач. Дело в том, что для строгой проверки корректности, нужно чтобы требования к программе были сформулированы так же строго. Насколько реально, для прикладных задач, строго сформулировать разумные ожидания пользователя? Как это вообще сделать? Кто должен этим заниматься, и сколько времени это займет? Не будет ли написание обычных юнит тестов быстрее? Что проще, набрать команду толковых тестировщиков, или набрать команду толковых математиков? И кому придется больше платить? Кто докажет, что требования в итоге сформулированы корректно? И кто докажет корректность интерпретатора, выполняющего функциональную программу? Где гарантия, что он одинаково работает на любом железе и при любом окружении, под нагрузкой, с временными задержками и т.д? Все равно, без тестирования, в условиях, приближенных к реальным, не обойтись.
Да и к тому же, сколько лет уже используется императивное программирование? До сих пор корректность работы программ была достаточной, и вполне неплохо проверялась с помощью тестов. Так что, это как минимум, не является острой проблемой, которую только ФП может героически решить. Что же касается научной сферы, где строгая проверка действительно актуальна, а проблемы скорости алгоритмов, наверное не очень критичны, то тут никто и не спорит, что функциональное программирование хорошо для этого подходит. Да и количество математиков на квадратный сантиметр, там побольше будет.
пользователю разумно ожидать, что какое-то действие программы будет выполняться, скажем минут за 5, а с нашими алгоритмами, оно выполняется за 30?Вы в каких-то стереотипах живёте… С чего Вы взяли, что ФП само по себе что-то делает в разы медленнее… для реальных задач скорее будет 5 и 6, и то при условии, что 5 — это что-то из серии C/C++, D, Go, Rust.
Более того существуют интерпретируемые императивные ЯП, которые как раз в разы медленнее, компилируемых (в том числе и функциональных). И ничего, прекрасно себе существуют и куча применений им находится.
Насколько реально, для прикладных задач, строго сформулировать разумные ожидания пользователя? Как это вообще сделать? Кто должен этим заниматься, и сколько времени это займет?Вообще, есть такая профессия, бизнес-аналитик называется. Впрочем, Вы там в сторону математического доказательства корректности углубились. Хотя по факту это не имеет смысла, достаточно того, что ФП упрощает то самое юнит-тестирование.
>для реальных задач скорее будет 5 и 6
Так ведь все зависит от конкретной ситуации. Если скорость устраивает, вообще не важно какая разница в скорости и насколько могло быть быстрее. Я просто указал на то, что утверждения типа: «корректность важнее быстроты», нужно всегда принимать с оговоркой: «если быстроты достаточно для обеспечения корректности». А вообще, лучше избегать выражений в форме: «X важнее чем Y». Гораздо правильней будет «стремись оптимизировать X, при Y, лежащем в допустимом диапазоне».
>Впрочем, Вы там в сторону математического доказательства корректности углубились. Хотя по факту это не имеет смысла, достаточно того, что ФП упрощает то самое юнит-тестирование.
С этим согласен. Углубляться в это особо и не хотел, просто так уж получилось. Мысль в эту сторону пошла. На практике, я никогда бы таким заниматься не стал. Думаю, и никто бы не стал.
Если скорость устраивает, вообще не важно какая разница в скорости и насколько могло быть быстрее.
Типичный случай когда скорость устраивает, это когда программа шлёт какие-нибудь SQL-запросы на сервер, и практическая скорость работы программы зависит от сервера, а клиент может быть и не быстрым. То ситуация когда, вообще нет практической нужды писать quicksort или нечто подобное на медленном языке.
Отсюда вопрос, как с подобным обстоит у ФП-языков? Как с практической поддержкой написания клиентов на ФП? Есть ли например, встроенные библиотеки (как в Delphi) позволяющие быстро написать клиентское приложение, без необходимости изобретать велосипед? И как в ФП с поддержкой UNICODE? (ага, классическая проблема Delphi — в Borland'е вообще забыли, что помимо латиницы, вообще-то существуют и другие системы письменности — от кириллицы до иероглифов) Что-то я не припомню примеров написания клиентов на ФП. Обычно сторонники ФП хвастаются чем-нибудь вроде "посмотрите, как красиво написан quicksort!". И при этом, я вообще не помню, ни одного примера, когда был бы пример написания клиентского приложения на ФП.
Так ведь все зависит от конкретной ситуации.Это да. Просто в реальных ситуациях программа выполняет кучу действий, какие-то быстрее в функциональном подходе, какие-то — в императивном, и в среднем разница не особо велика.
+ не все задачи требуют «максимально быстро», бывает просто «достаточно быстро»
Например, разбор заголовка MP3-файла можно сделать декларативно, но если это надо сделать для 100500 файлов, то придётся вспомнить про fseek. Другими словами, инструмент зависит от задачи, и чем больше у вас инструментов, тем легче выбрать наиболее подходящий под конкретную задачу.
Неявно подразумевается, что пользователю разумно ожидать, что какое-то действие программы будет выполняться, скажем минут за 5, а с нашими алгоритмами, оно выполняется за 30? Корректность в данном случае нарушена, т.к. программа не делает того, что от нее требуется.
Нет, корректность не нарушена. Программа корректно делает то, что от нее требуется. Да, делает медленно — но это вопрос повышения быстродействия при сохранении корректности результата.
Это верно только для каких-нибудь математических задач. Дело в том, что для строгой проверки корректности, нужно чтобы требования к программе были сформулированы так же строго. Насколько реально, для прикладных задач, строго сформулировать разумные ожидания пользователя? Как это вообще сделать?
Для большинства прикладных задач (от решения которых не зависят жизни людей или дорогостоящее оборудование) доказывать корректность всей программы необязательно. Достаточно проверить корректность ключевых алгоритмов. Если писать эти алгоритмы в функциональном стиле, т.е. на «чистых» функциях — их корректность (всегда вернут такой-то результат при таких-то значениях входных параметров) может быть формально выведена благодаря referential transparency, без всяких математиков. Корректность «грязных» функций в общем случае доказать невозможно.
До сих пор корректность работы программ была достаточной, и вполне неплохо проверялась с помощью тестов. Так что, это как минимум, не является острой проблемой, которую только ФП может героически решить.
До сих пор из-за багов в софте теряются огромные деньги. Если есть парадигма, которая может существенно сократить количество определенных типов багов, использовать ее экономически оправдано.
Но если время работы является одним из требований, и оно не выполняется, то программа не выполнила своих функций. Либо, если все таки выполнила, то требования по времени были изначально неправильными и излишними. В любом случае, если производительность устраивает, то никаких вопросов нет, я просто говорил о том, что производительность может и не устраивать. Да и обсуждаемая статья именно об этом.
Со всем остальным я в принципе могу согласиться, особенно про ненужность формального доказательства корректности для большинства задач. Я об этом вообще заговорил только в контексте того, что это было заявлено, как решающее преимущество. Использовать аргумент, что это вообще не нужно — не стал, т.к. если бы это было реально достижимо для всей программы, это был бы серьезный плюс. Но если говорить об отдельных алгоритмах, то и спорить не о чем, можно тогда только эти алгоритмы и реализовывать на ФП, а остальное как удобнее. Это уже мультипарадигменность.
Что по поводу чистых функций и их проверки, то я лично не считаю чистые функции неотъемлемым атрибутом именно ФП. Мне ничего не мешает написать обычный метод, или даже процедуру, если хотите, которая будет принимать значения в качестве параметров и возвращать результат, не взаимодействуя с какими-либо глобальными данными и не имея побочных эффектов. Внутри это будет чистый алгоритм. А для внешнего наблюдателя — чистая функция. Любой процедурный язык это позволяет. Плюсы ФП не в том, что они позволяют писать такие функции, а в том, что они позволяют это делать быстрее и проще. И это становится актуально, когда таких функций надо писать много. Чтобы точно избежать недопонимания, это я не к тому написал, что ФП не нужно, а к тому, что даже если язык вообще не позволяет писать в функциональном стиле — это не повод его менять, по крайней мере, если нет реально серьезной потребности в функциональщине.
Если язык, претендующий на высокоуровневость, не позволяет быстро и просто написать чистую анонимную функцию высшего порядка с замыканием, то, имхо, это повод его менять.
При этом пара утверждений однозначно верны:
1. Реальные объекты имеют состояния и эти состояния могут меняться с течением времени.
2. Реальные данные имеют свойство меняться с течением времени.
Т.о. получается, что все задачи динамики (завязанные на времени) являются по своей природе императивными. Т.е. класс «чисто функциональных» задач очень скуден.
Из чего мы приходим к выводу, что имеет смысл внедрять элементы ФП в мультипарадигменные языки для упрощения анализа статики, но при этом чисто функциональные яп не применимы к большинству реальных задач.
Реальные объекты имеют состояния и эти состояния могут меняться с течением времени.
Это вопрос терминологии. Можно считать, что состояние реального объекта меняется, а можно считать, что объект получает новое состояние. И, как правило, состояние объектов реального мира, моделируемое с помощью вычислительных систем, меняется не с течением времени, а в результате наступления каких-то событий. На популярных современных архитекторах данные тоже меняются не с течением времени, а по событию как минимум «наступил заданный фронт тактового генератора».
1) Я услышал, что чайник вскипел — надо пойти налить кипяток в кружку.
2) Домой вернулась жена из магазина, хочет попить чаю. О, чайник уже горячий, тогда можно не греть, сразу наливаем в чашку.
Второй случай существенно отличается от первого, т.к. жене не нужно мониторить состояние чайника, реагировать на изменения оперативно, или запоминать все состояния, это не является её приоритетом. Ей не нужно знать, что чайник вскипел, ровно в тот момент, когда это произошло. И я не шлю ей СМС: «чайник вскипел», а даже если я так сделаю, то она не бросит все, и не прибежит из магазина пить чай. Ей лишь надо узнать его актуальное состояние в тот момент, когда он понадобился, и быть уверенной, что состояние именно актуальное. И таких сущностей и процессов полно, когда своевременно знать о завершении операции надо только тому, кто операцию инициировал, а остальным нужно лишь актуальное состояние сущности в момент обращения. И ситуация, когда каждый находящийся в доме, имеет доступ к чайнику и может его согреть — это тоже естественно. За доступом к нему совершенно не надо следить и ограничивать его (хотя мы и можем это сделать при необходимости). Задач такого рода тоже полно. Это легко и эффективно решается с помощью ссылок и мутабельности.
ООП вообще на акторах реализуется даже лучше, чем на классах. И мутабельность для этого не нужна.
В плане практических бизнес-задач у ФЯП проблем никаких нет.
Единственная проблема в ФП — реализовывать алгоритмы, построенные на базе мутабельности и массивов. Какие-то из них может даже не получиться реализовать за ту же сложность. Но, это довольно узкий класс задач… вспомните, когда Вы прошлый раз Кнута перечитывали?
Отличается полной изоляцией и последовательной обработкой сообщений (методов в терминах ООП). Подробнее:
- Если в методе heat объекта Teapot произойдёт исключение, которое никто не перехватит, то упадёт вся программа. Если при обработке сообщения heat актора Teapot произойдёт исключение, то упадёт только он никак не затронув другие части программы. Причём стандартной практикой является наблюдение за акторами с помощью супервизора, который перезапустит актор Teapot в исходном состоянии и он снова сможет обрабатывать сообщения. За этим стоит целый подход к разработке ПО, под названием Let It Crash!
- Если два параллельных потока захотят сменить состояние объекта Teapot, то программа упадёт с race condition. Если два параллельных потока (процесса в терминах OTP) захотят сменить состояние актора Teapot, то эти смены будут применены последовательно, т.к. любое сообщение (вызов метода) попадает в очередь сообщений актора и он обрабатывает эту очередь по порядку.
Ну а в плане преимуществ, которые может дать ООП, ничем не отличается :-)
ситуация, когда каждый находящийся в доме, имеет доступ к чайнику и может его согреть — это тоже естественно.
Не естественно. Естественно, если «каждый» — это единственный человек, бывающий в доме. А так, если кто-то уже греет себе чайник, то у кого-то ещё возможности его согреть прямо сейчас нет, надо или просто дождаться когда чайник согреется (возможно даже вылив всю воду себе, если тот, кто греет не заблокировал выливание воды) и воспользоваться результатом, или ждать когда закончится цикл согревания и инициировать новый (если усеешь вклиниться между другими), или прерывать процесс инициированный другим, доливать воды на двоих и запускать новый цикл.
Это лишь на бытовом уровне кажется, что каждый в любой момент может согреть себе чайник. Попытайтесь создать императивный алгоритм для управления «умным чайником» для семьи из трех человек, не имеющих привычки пить чай все вместе, но иногда пьющих и по одному, и по два, и по три.
По-моему Вы слегка путаете мутабельность данных с изменением состояния.
Мутабельность данных означает, что Вы можете полностью перезаписать значение переменной в ОЗУ и все переменные, указывающие на этот же адрес в памяти, также получат новое значение. Именно мутабельности данных нет в ФП, а состояние изменяется без проблем, просто оно не разделяемое, а по-настоящему инкапсулировано в лучших традициях ООП )))
Подумайте, знаете ли Вы хоть один императивный язык, поддерживающий инкапсуляцию в полной мере? Или метод одного объекта может вызвать метод другого объекта и передать ему часть своего состояния по ссылке? А ведь это прямое нарушение инкапсуляции, т.к. другой объект может спокойно и без каких-либо разрешений или уведомлений изменить значение по этой ссылке.
Естественно, если ваша основная задача выжать максимум производительности, то вы пишете на C, тут без вариантов. Если же вы применяете какие-то высокоуровневые ЯП, то совсем не исключено, что ФП-решение покажет лучшую производительность на ваших задачах.
P.S. Кстати, какой смысл говорить о чистых языках как-будто о ФП вцелом, если из известных чистых есть только Haskell?
(Например, Delphi сам по себе язык не быстрый, но можно очень быстро написать код который будет работать корректно)
И ещё: имхо, ФП-код выглядит как красивый эффектный ребус (для меня). Насколько быстро этот ребус можно написать? И насколько углублённое знание лямбда-исчисления и прочей математики требуется чтобы писать быстро? И насколько скорость написания зависит от того насколько человек хорошо знает лямбда-исчисление и тому подобное? (на императивных языках возможно писать и не зная глубоко теорию)
Скорость написания примерно такая же, как и на императивных языках, после того как осознаёшь функциональную парадигму и перестаёшь пытаться перетянуть туда императивные привычки.
И насколько углублённое знание лямбда-исчисления и прочей математики требуется чтобы писать быстро?Просто не лезьте в Haskell и такие вещи как лямбда-исчисление, монады, функторы и т.д. Вас не потревожат )))
Т.е. ответ по существу — Вы можете вообще не знать что такое лямбда-исчисление и теория категорий, и прекрасно себя чувствовать на функциональных языках типа Elixir или Clojure, а также и на гибридных типа F#. Из ребусов только некоторые особенности записи могу вспомнить, но они скорее непривычные, чем сложные, и довольно легко осваиваются.
С хештаблицами то же самое. Это лишь массив списков. Если компилятор сможет увидеть, что к старым версиям таблицы не обращаются и существует только одна последняя — нет проблемы сгенерировать код равный императивному.
Со слабыми хештаблицами сложнее, ибо в ФП нет никакой памяти, объектов по указателям, ибо мы от всего этого абстрагировались. Нужно или вручную, или вводить в ЯП тип объектов, держащих значения, плюс тип слабых ссылок, конструктор которого принимает объект и любое значение и функцию, которая будет должна подменить значение и заменить следящий объект, если объект подвергается сборке мусора. Тогда делаем список на этих слабых ссылках, указывая фактором разрушения объект, хранящиеся в следующем элементе. Как вызывается коллбек — выбрасываем звено с этим объектом.
Нет, конечно, настоящий fortran программист может писать на fortran на любом языке. И в хаскеле можно напрямую обращаться к памяти и писать итеративные программы.
Но чем тогда хаскель отличается от си кроме чистоты? На си тоже можно передавать функции как аргументы. И возвращать тоже. Так что си — функциональный язык? Лисп и питон ещё ближе друг к другу. Почему одно считается функциональным языком, а другое — императивным?
Функциональный язык программирования — это типичный баззворд. Функциональный язык ничем не отличается по возможностям от нефункциональных. Разве что по удобству тех или синтаксических конструкций. В лиспе лямбда функцию можно записать как угодно, а в питоне только одной строкой. Разница не принципиальна.
Единственное отличие хаскеля, на которое всегда напирают — это чистота, а не функциональность. Но эту чистоту постоянно нарушают ради производительности. Кстати, чистые и мутирующие функции — это слишком грубое деление. Бывают языки с системой эффектов, где разнообразные нарушения чистоты классифицированы и контролируются по-отдельности. И для этого вовсе не обязательно быть функциональным языком.
Другими словами, вопрос только в качестве, удобстве и полноте поддержки той или иной парадигмы в языке.
Попробуйте реализовать построение суффиксного дерева на функциональном чистом языке за линейное время. Я вот ещё ни одной реализации не встречал. А на императивных «грязных» языках — запросто.
Я уже несколько раз тут в комметах писал… не надо пытаться реализовывать императивные алгоритмы в функциональном стиле. Ничего хорошего из этого не получится. И это ни разу не аргумент против ФП.
Так я и не требую реализовать конкретный алгоритм. Я говорю — попробуйте решить задачу с ограничением времени в O(n). Можно даже амортизацию применять. Я находил функциональные алгоритмы с временной сложностью O(n*log(n)). Принципиально медленнее.
> Другими словами, вопрос только в качестве, удобстве и полноте поддержки той или иной парадигмы в языке.
Вот это правильно. Но когда воспевают хвалу ФП, говорят не про удобство разработки, а про принципиальные возможности. Вроде доказательств, оптимизации и прочих серьёзных плюшек. А все эти плюшки происходят не от ФП, а от чистоты и ленивости. Которые не обязательны для ФП. И даже в самом-самом ФП языке хаскель на практике часто отказываются от ленивости и чистоты, чтобы получить скорость. Ленивость остаётся лишь местами. А это уже не так сильно отличается от совсем-совсем не ФП языков вроде С++, который может достигать ленивость и чистоту местами через boost.
Я говорю — попробуйте решить задачу с ограничением времени в O(n). Можно даже амортизацию применять. Я находил функциональные алгоритмы с временной сложностью O(n*log(n)). Принципиально медленнее.Извините, но не буду тратить на это время. За 10 лет практического опыта, мне эту задачу ни разу не приходилось решать для реального проекта. Поэтому лично мне вообще не интересно какая у неё алгоритмическая сложность.
Если Вы в каждодневной работе часто сталкиваетесь с подобными задачами и сложность функциональных алгоритмов Вас не устраивает, то возможно под Ваши задачи лучше подойдёт С++.
Мне тоже не приходилось, есть готовые библиотеки. Для си они имеют сложность O(n), для хаскеля — O(n*log(n))
> Если Вы в каждодневной работе часто сталкиваетесь с подобными задачами
Не сталкиваюсь. Один раз потребовалось, когда реализовывал свой diff. Но алгоритм классический, пусть и низкоуровневый. Имеет множество практических применений. И потому показателен — если за всё это время никто не смог написать на хаскеле чистый алгоритм, то значит это невообразимо сложно. А значит на практике нерешаемых задач окажется ещё больше, просто они все разные, не стандартизированы, и такой концентрации внимания не получили.
Так что надо понимать, что за чистоту приходится платить не только неудобными громоздкими конструкциями, но и асимптотикой.
Другими словами, абстрактный выбор между функциональными и императивными языками — это бессмысленный холивар. Язык надо выбирать исходя из своих конкретных задач.
И в этот момент программа на хаскеле перестаёт быть чистой. Что вполне допустимо с практической точки зрения. Но хоронит насмерть аргументы о том, что чистота спасёт мир. Не будет вам чистоты для практических задач. А значит и не будет означенных теоретических преимуществ на практике.
2) Необходимость использовать FFI на практике возникает крайне редко. И ничто не мешает скопировать аргументы и изменять in-place их копию, что вполне себе сэмулирует чистоту на уровне абстракции самой программы: «вызвали функцию, она вернула результат, никак не повлияв на свои аргументы». Все преимущества чистоты следуют сугубо из того, как это работает на верхнем уровне абстракции, а не из того, как это достигается на уровне ассемблера.
Остаётся только повторить, так в чём уникальность и неповторимость FP? Оно чуть удобнее в какой-то области задач? Подобное можно про каждую парадигму программирования сказать.
Остаётся только повторить, так в чём уникальность и неповторимость FP? Оно чуть удобнее в какой-то области задач? Подобное можно про каждую парадигму программирования сказать.В принципе да, поэтому чем больше парадигм знаешь, тем лучше. Императивную хотя бы частично (процедурную, структурную, объектно-ориентированную на классах) знают все, декларативную — пока немногие.
По-моему глупо рассчитывать, что все задачи можно решить в рамках какой-то единственной парадигмы и при этом ни разу не загнать себя в дикие извращения. У каждой есть своя сфера применимости.
https://ro-che.info/ccc/25
вопрос не раскрыт, вся суть, которую можно было бы обсуждать спрятана за фразой «Для некоторых приложений это непозволительно медленно.»
автор отмечает «Впрочем, это не самый серьезный недостаток, учитывая то, что большинство разработчиков никогда не пользовались слабыми хэш-таблицами…»
я не настоящий сварщик, но acid-state заявляет s«tronger ACID guarantees than most RDBMS offer» логично что для одновременного доступа из разных потоков?
со сложностью системных компонентов написанных один раз для всех можно мириться, проседание производительности это проблема, но опять же нужны примеры где это является узким местом
странно обвинять подход в том, что мы его не разрабатываем, это конечно минус, но немного из другой сферы
автор сам оговаривается, что это не всегда и не совсем недостаток, память не очень дорога, можно заплатить памятью за качество
тут вообще проблема не обозначена, какие именно проблемы ФП не позволяют производительно параллелиться? И о какой параллельности речь, тут пишут что warp обгоняет nginx, тоже параллельность.
да, пожалуй эта проблема есть, фанаты ФПшной математики не всегда уважительно относятся к тем, кто не получил соответствующего образования.
Но, как отмечает сам автор, ситуация улучшается.
Также как и пункты 5, 8 это не проблемы самого ФП, а проблема малой распространённости
Недостатки чистого функционального программирования