Как стать автором
Обновить

Функциональное программирование — то, что вам (наверно) рассказывали. Если вы слушали

Время на прочтение 16 мин
Количество просмотров 30K
Всего голосов 54: ↑50 и ↓4 +46
Комментарии 299

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

>В первом и втором в очередной раз рассказано, что ФП — дрянь
Вообще-то, обе статьи — стеб. Странно, что вы этого не поняли.

>А вот многих математиков такое определение не слишком удовлетворило. Видимо, понятия, которыми оперировал Тьюринг, показались им недостаточно… абстрактными.
Вообще-то эти «математики», а именно Алонзо Черч, свои работы по лямбда исчислению выполнили еще до Тьюринга. Более того, Алонзо Черч был учителем Тьюринга. Не то чтобы это было существенно, но последовательность исторических событий выглядит странно.
Да, я этого не понял. Видимо потому, что часто слышу ровно того же порядка аргументацию без всякого стёба.

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

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


В итоге дочитам до конца, всё стало достаточно очевидным. Если вас и это не убедило, то в конце есть комментарий на +89 для развеивания последних сомнений.

Простите, не понял Вашу мысль

Статьи которые вы линканули в качестве аргумента "вон фп ругают" это весьма очевидный стёб на уровне ущербно-ориентированного программирования.. Но ни там фп не ругают, ни тут ООП не ругают.


Причем написано достаточно толсто, поэтому если вы приняли это на веру, то вас очень легко задеть любым невосхищением ФП)

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

Значит автор хорошо выполнил свою работу, разве нет?

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

Заголовок спойлера
def fibonachi2(a, b):
    yield a
    yield from fibonachi2(b, a+b)


def fibonachi():
    yield from fibonachi2(1, 1)


def nfibonachi(n):
    result = []
    for i, number in enumerate(fibonachi()):
        if i == n:
            return result
        result.append(number)



Эх не дочитал статью дальше, прежде чем свой пример писать :-)

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


fair_id = reduce(lambda a, b: (a if a
  == b else raise_(DepersonException('Multiple fairs are not supported'
  ))), fair_ids)

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


fair_id = fair_ids[0]
if any(id != fair_id for id in fair_ids):
  raise (DepersonException('Multiple fairs are not supported')

Такие дела.




Если вдруг вопросы, почему редьюс лучше: если убрать строчку с проверкой, то исчезнет переменная fair_id, и попытка её использовать будет ошибкой инетрпретатора (предпочел бы компиляции, ну ладно уж). А во втором слуачае будет просто иногда неправильно отрабатывать скрипт.




Что до ленивости — то ленивость списков выражается через явную конструкцию, добавленную для этого — итераторы/генераторы. А вот для не-списков (деревьев, например) такого уже нет. А еще лучше: для управления флоу, эти ваши when/until функции.

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

Это решается заведением отдельной функции:


def take_any_copy(list):
    a = list[0]
    if any(a != b for b in list):
        raise DepersonException('Multiple items are not supported')
    return a

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

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


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

А почему вы рассматриваете только комментирование? Точно так же я могу заменить ваш reduce на что-то ещё и всё поломать. И никакой компилятор мне не помешает...

Я оцениваю количество строк, в которых существует потенциально невалидная переменная. В частности a является невалидной до тех пор, пока она не проверена условием. Количество невалидных строчек одна (та, где мы проверяем на any).


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


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


К слову, в идеале я хотел бы написать [fair_id] = set(fair_ids) с возможностью переопределить эксепшн который должен вылететь, тогда и читаемее было, и понятнее, но подходящей функции я не нашел. А трай кетчи очень не люблю.

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


Я вот опасной строчкой считаю ту, которая не пойми что делает. Потому что наличие такой строчки ускоряет переход кода в категорию "legacy, наверное для чего-то нужно"

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

Очевидно? Вы или гений, или обманываете.

Я не гений, но и не обманываю. Давайте еще раз код посмотрим:


fair_id = reduce(lambda a, b: (a if a
  == b else raise_(DepersonException('Multiple fairs are not supported'
  ))), fair_ids)

я читаю:


номер ярмарки — ага, новая переменная
равен — ага, щас посмотрим значение
свертке списка fair_ids — ага, значит смотрим на лямбду свертки.


чтобы понять лямбду свертки нам достаточно посмотреть, что происходит с двумя переменными: a и b.


(a if a == b else raise_(DepersonException('Multiple fairs are not supported') — если оба параметра совпадают, то возвращаем один из них, иначе бросаем исключение что параметры не могут различаться.


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


foo(a,b):
  return a if a 
    == b else raise Exception('Multiple argument are not supprted')

А больше там ничего и нет — присваивание и редьюс.

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


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


В то же время, альтернативный вариант читается безо всяких сложностей, просто как текст.

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

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


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

ИМХО это не сложнее чем сделать вывод "если исключения не было — значит, все параметры совпадают с первым".




Ладно, возможно, вы правы. Я хотел написать что-то в таком духе:


var fairId = 
  fairIds.Distinct().SingleOrDefault() 
  ?? throw new DepersonException("Multiple fairs are not sup");

Возможно, действительно с отдельной функцией с валидацией читалось бы лучше. Спасибо!

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

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

А можно конкретнее пример где это может выстрелить? А то такого, чтобы тайпчекнулось, но неправильно работало трудно представить. Кроме как foo(bar(1,2), 3), где обе функции это варарги, и `bar() еще и число возвращает. Но так просто не стоит писать. А более реалистичного примера в голову что-то не приходит.

В Питоне тайпчекером на пол-ставки, увы, вынужден работать программист. Отсюда и ненависть к неидеоматичному коду.


В C# богатые традиции по созданию перегрузок функций наложены на новейшие возможности компилятора по созданию параметров со значениями по умолчанию. Я просто не хочу задумываться на тему "тайпчекнется или нет в случае ошибки", лучше пересчитаю на всякий случай скобочки :-)

В Питоне тайпчекером на пол-ставки, увы, вынужден работать программист. Отсюда и ненависть к неидеоматичному коду.

Ну, мне тут питонисты недавно продавали mypy, довольно крутая штука. Вот например: https://mypy-play.net/?mypy=latest&python=3.8&gist=6dea37f9f65b88bbb754e2504353e4ea


В C# богатые традиции по созданию перегрузок функций наложены на новейшие возможности компилятора по созданию параметров со значениями по умолчанию. Я просто не хочу задумываться на тему "тайпчекнется или нет в случае ошибки", лучше пересчитаю на всякий случай скобочки :-)

Ну, одна из причин почему я предпочитаю статически типизированные япы — нравится перекладывать работу на машину, пусть думает, она в отличие от меня ошибается куда реже)

НЛО прилетело и опубликовало эту надпись здесь

А можно конкретный пример? А то я ваше мнение учту, но чтобы им пользоваться нужен пример, а то безосновательно получится.

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

Я придерживаюсь более радикального мнения: ДТ была ошибкой и в 2020 в языках общего назначения не нужен. :)

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


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

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

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

  1. Как минимум, появляется забавный класс ошибок: передача в функцию/метод аргумента не того типа. Это звучит не так страшно примерно до того момента, как кодовая база перешагивает за 100 тысяч строк. Единственный выход — обмазываться тестами (большим количеством, чем для статики) и ставить ассерты. Отсюда и легендарное питоновское кидание стектрейсами по любому поводу.


  2. Проверка типов происходит в рантайме и это сильно не бесплатно. И ещё сильнее бьёт по скорости создания объектов: вместо простых vtable'ов приходится хранить кучу данных о методе, вплоть до его имени и заполнять эти данные при создани объекта. Хочется распихать миллион записей по структурам и обрабатывать в памяти? Нет, работай с сырыми данными, энтот ваш ООП не нужен.


  3. Статическая типизация, оказывается, обладет свойством самодокументации: изучаем API библиотеки? Сами по себе возвращаемый тип и типы аргументов говорят об ограничениях на входные/выходные данные, а так же о работе функции. В ДТ приходтся лезть в документацию на каждый чих. И писать её на каждый чих.


  4. Ещё интересное следствие: больший простор творчества для говнокода. Поменять в существующем методе возвращаемый тип? Или поставить возвращаемый тип в зависимость от входных данных? Питон считается языком с низким порогом вхождения, но на деле требует опыта и самодисциплины от разработчика. Плохая репутация питона во многом растёт именно отсюда.


НЛО прилетело и опубликовало эту надпись здесь

Я всё-таки предпочитаю, чтобы функция возвращала обобщённый тип, а не фиг-его-знает-что-угадывай-сам. =)

НЛО прилетело и опубликовало эту надпись здесь

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

В итоге в питон чате единогласно сказали, что это говнокод, и так не пишут.

Правильно сказали. Очень тяжело читать подобное, особенно если подобного много.

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

А вот и undefined behavior подьехал, здравствуйте однако.

Такие дела.

Покажите в каком моменте это уб. Я вообще не уверен, что в питоне хоть что-то уб.

иногда неправильно отрабатывать

Это и есть undefined behavior. В каком именно месте не знаю — любви к чтению и разбору говнокода не имею, опираюсь лишь на ваши слова.

Нет, UB и "неправильно работает" это совершенно разные вещи.

Вы написали не «неправильно работает», а «иногда неправильно работает». И в чем же разница с уб?

Разница в том, что при наступлении UB разрешается любое поведение программы, даже противоречащее спецификации языка


Если неправильно работающий скрипт имеет хоть какое-то поведение — это уже не UB.

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

Я даже в гугл переводчик залез, чтоб удостовериться: «Undefined behavior» — «Неопределенное поведение». Если для различных входных данных соответствующих спецификации функции в одних случаях результат будет соответствовать ожидаемому, а в других — чушью, то является ли это «определенным поведением» или «неопределенным поведением» с точки зрения пользователя функции?

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

Вы — пользователь. Вам дали функцию — черный ящик. У вас есть набор гомогенных данных, соответствующих спецификации функции. Вы берете батч данных и отдаете функции — она возвращает корректный результат. Вы берете второй батч и отдаете функции — она возвращает не коррекный результат. Вы берете третий батч… Что вы будуте ожидать? Success? Error? Пока не выполните — не узнаете. Любой результат работы этого черного ящика будет не ожидаемым, так как вы просто не знаете чего ожидать :) «Иногда работает, иногда не работает» — это и есть неопределенное поведение. Если убрать слово «иногда», то да, она либо работает, либо нет — никакой неопределенности.

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

Мы не знаем что она возвращает. Мы знаем лишь, что она «иногда неправильно работает». Она может вернуть корректный ответ, может ничего не вернуть, может убить собаку пользователя или сделать ему минет. Иногда «undefined behavior» — это просто неопределенное поведение, а не ссылка на спецификации и википедию. Рад что таки смог донести свою мысль.

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


Касаемо питона, пункт 6.15 документации:


Python evaluates expressions from left to right. Notice that while evaluating an assignment, the right-hand side is evaluated before the left-hand side.

In the following lines, expressions will be evaluated in the arithmetic order of their suffixes:
expr1, expr2, expr3, expr4
(expr1, expr2, expr3, expr4)
{expr1: expr2, expr3: expr4}
expr1 + expr2 * (expr3 - expr4)
expr1(expr2, expr3, *expr4, **expr5)
expr3, expr4 = expr1, expr2

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

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

Почему "имитирующая"-то?

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

Как я уже отвечал коллеге, это не более чем терминологический спор, непродуктивный до момента, пока Вы не предоставите удовлетворительное определение ФП. Я такое определение дал — это программирование в рамках ленивой модели вычислений. Это определение неидеально и, очевидно, неполно, и я даже на нем не пытаюсь настаивать. Из него выбивается часть языков, которые душа требует не класть в один ящик с типичными императивными. Но в то же время оно позволяет обосновать наличие свойств, инструментов и практик, характерных для функциональных программ. Если у вас есть другое, более конструктивное определение, из которого имеются интересные следствия (иначе зачем оно нужно?), то я с огромным удовольствием его приму.
А программы на функциональных языках, для теорий типов которых выполняется strong normalization theorem

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

А почему вы считаете, что функция-генератор "полностью выполнилась" когда вернула управление, а не когда исполнение кода дошло до её конца?

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

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

Я не знаю питон, вопрос без подвоха.
В данном случае никаких значимых следствий мне не известно. Под «имитацией» ленивых вычислений я имел ввиду то, что в императивном по своей природе языке появляется синтаксическая конструкция, которая упрощает реализацию поведения, характерного для языков, явным образом использующих ленивую модель вычислений в качестве основного механизма функционирования. В статье я постоянно делаю на этом акцент: заимствование из одной парадигмы в другую — вещь совершенно нормальная. Разница только в том, является ли такой механизм естественным следствием какой-то фундаментальной концепции, либо же он спроектирован сознательно для реализации определенных возможностей языка.
Неприязни к ФП не испытываю, но есть одна «претензия», из-за которой в изначально функциональные языки я не лезу. Звучит она примерно так:

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


С чего вы это взяли? Любая программа, реализованная на понятном человеку языке, выполняется на императивном железе, которое понимает только бинарные последовательности. Следует ли из этого, что «в случае работы над чем-либо сложным» программист должен в голове транслировать написанную программу в двоичный код? Напротив, чем сложнее задача, тем более абстрактными категориями приходится оперировать для ее решения.
Ну вообще в седые времена когда lisp лопать программно было затруднительно делались оптимизированные под lisp машины. И там lisp запускался практически на железе.
Следует ли из этого, что «в случае работы над чем-либо сложным» программист должен в голове транслировать написанную программу в двоичный код?

Следует. И про это часто пишут на хабре.

Можно и более преземлённые вещи вспмонить. Например, в геймдеве (и ещё много где) очень важно физическое рассположение данных в памяти и последовательность их отправки на процессор.

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

Но я не про это. Не надо путать смену представления логики программы и смену самой логики: «трансляцию» и «интерпретацию/компиляцию», если угодно.

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

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

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


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

разработчик при разработке должен держать в голове две модели вычислений вместо одной


Это не более чем Ваши домыслы.
Давайте начнем с простого момента. Это так только для компилируемых языков. И то только для более-менее простых без ООП. Как только у вас появляется ООП или язык использует свою виртуальную машину, то все приплыли.

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

Весь этот интерес к функциональному программированию и корутинам в том числе не от хорошей жизни.
  • Во-первых куче JS-разработчиков незнание нижележащей платформы не мешает работать :dunno:


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


    Сами задачи на js, чтобы было легко исполнить их в браузере:
    https://jsfiddle.net/djth9oq7/
    https://jsfiddle.net/3smxuh1o/


  • Во-вторых возьмем другую парадигму: ООП. Никто вроде не говорит "почему вы выполняете ООП код на императивной машине"? Хотя ООП отличается прям настолько же, в этих ваших процессорах нет никаких менеджеров, адаптеров, визиторов и прочего. Но как-то работает, хотя часть паттернов (например предпочтение AoS нежели SoA) прямо проитиворечит нижележащей архитектуре. И вполне неплохо.


  • Ну и наконец можно просто пойти на бенчмаркгеймс и посмотреть как там себя хаскель или скала показывают. А показывают на уровне джавы. Что кмк вполне себе приемлемый уровень. Можно поспрашивать знакомых, насколько им сложно оптимизировать ФП код. Судя по моим знакомым — проще, чем плюсовый. Потому что локал ризонинг и ссылочная прозрачность.


Первая — branch prediction, вторая — кэш, если я правильно понимаю?

Именно так.

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

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


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

ответил выше
НЛО прилетело и опубликовало эту надпись здесь

Спасибо за статью, любопытно. Хотя я не со всем согласен:


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

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


Я больше склоняюсь к тому, что есть языки, где ленивость включена/выключена по-умолчанию для всех типов, а для конкретных она включается/выключается: банг операторы, seq, лэзи типы, и вот это всё. То есть вопрос в том, какую парадигму язык по-дефолту рекомендует. Тем не менее, на хаскелле спокойно можно писать строго (особенно этим любит заниматься 0xd34df00d ), а на этих ваших питоношарпах — лениво.


Нужны ли монады в императивном мире? У меня нет устоявшегося мнения по данному вопросу. Автор этого поста уверен, что нужны.

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


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

Я сам пишу сейчас фуллтайп на сишарпе, в основном, и я не пишу ни монад, ни either'ов — продолжаю плакаться и колоться писать частичные функции с эксепшнами, и никакими монадами не упарываюсь: я прекрасно знаю, в какое нечитаемое месиво это превращается, лучше уж жить с известными недостатками, чем с этим.

Не понял смысл последней фразы. В нечитаемое месиво превращаются монады в неподходящем для этого языке?

Вообще я пытаюсь уже во втором-третьем примерно проекте использовать either/try и т.п. в Java, не могу сказать, что я доволен на 100%, но в целом положительный эффект все-таки наблюдается. Как минимум, я на сегодня предпочту сигнатуру функции в виде Try, по сравнению с просто Integer и возможностью кинуть исключение. Использующий это код получается более простым, и намерения автора выражаются более ясно.

Просто рано или поздно оно превращается в такое:


img


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


Лучше уж эксепшны, право слово. А еще лучше брать подходящий язык: хотя бы фшарп, где работа с АДТ не вызывает такой боли.

Да, я понял, о чем это было сказано. До такого у меня не доходит, хотя тенденция такая налицо.

Э-э-э, а там хоть один из возвращенных параметров где-то дальше используется?


Если я ничего не путаю, этот код можно заменить на цикл.


Или это настолько нагруженный микросервис, что виртуальные вызовы в таком количестве тут недопустимы? Тогда надо собрать и по-билдить Expression.

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


applySecurityRulesOnSection section = do 
  _ <- applyPayrollFieldSecurity (section  ^. payroll)
  _ <- applyTermFieldSecurity (section ^. terminationHistory)
  _ <- applyPositionFieldSecurity (section ^. positions)
  _ <- applyRateHistoryFieldSecurity (section ^. rateHistory)
 ...

Если бы from в шарпе был чутка удобнее, то и в нем можно было бы.


ИСЧХ в языке с монадками в языке есть возможность получить бесплатную параллелизацию:


applySecurityRulesOnSection section = liftA4 work a b c d where 
  a = applyPayrollFieldSecurity (section  ^. payroll)
  b = applyTermFieldSecurity (section ^. terminationHistory)
  c = applyPositionFieldSecurity (section ^. positions)
  d = applyRateHistoryFieldSecurity (section ^. rateHistory)
  work = ...

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

Еще вот так можно.

Это у вас циклов нету, а я предлагаю добавить:


foreach (var handler in new Func<Either<ControlException, Unit>>[] {
    () => _payrollSubsectionSecurityService.ApplyPayrollFieldSecurity(section.Payroll),
    // ...
}) {
    var result = handler();
    if (!result.Succeded) return result;
}

После преобразования к такому виду код точно так же можно будет при желании "распараллелить".


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

ну делаете


Either<ControlException, Unit[]> = sections
   .Select(section => _payrollSubsectionSecurityService.ApplyPayrollFieldSecurity(section.Payroll))
   .Sequence();

и всё. Не добавляют циклы тут никакой сложности.

Э-э-э, это вы о чём? Откуда взялись sections во множественном числе?


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

А, понял, не так прочитал.


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

Ну да, в Хаскеле это был бы вызов forM. Однако, этой механической работы совсем немного, особенно если сравнивать с тем кошмаром из скриншота.

Тот кошмар из скриншота можно было бы заменить на цепочку биндов (вместо match), тогда тоже такого треша бы не произошло. Но тут уж как написано, так написано)

Вообще то в шарпе уже есть подобие монад. Только вместо bind надо реализовать Select и SelectMany:
public Either<L,T> Select<T>(Func<R, T> map)
{
    if (IsLeft)
        return new Either<L, T>(left);
    else
        return new Either<L, T>(map(right));
}
public Either<L, R3> SelectMany<R2, R3>(Func<R, Either<L,R2>> select, Func<R, R2, R3> project)
{
    if (IsLeft) return new Either<L, R3>(left);
    var selected = select(right);
    return selected.IsLeft ? new Either<L, R3>(selected.left): Select(t => project.Invoke(right, selected.right));
}

и тогда ваш код можно записать как:
    var res = from a in applyPayrollFieldSecurity(section.Payroll)
        from b in applyTermFieldSecurity(section.TerminationHistory)
        from c in applyPositionFieldSecurity(section.Positions)
        from d in applyRateHistoryFieldSecurity(section.RateHistory)
        select ...

Верно, но работа с Either в дотнете всё же не очень удобна, особенно если типы ошибок вдруг различаются. Но да, с линукшной ду-нотацией выглядет очень схоже. Жалко, сишарп так и не научился в плейсхолдер _ в LINQ. Причем везде в других местах оно работает:


_ = 5;
_ = 10;

А вот в LINQ — почему-то нет.

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

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

Обязательно ознакомлюсь, спасибо за наводку.
Мы ведь не обсуждаем, что работает хорошо, а что плохо. Это зыбкая дорожка.
Тут ведь простой терминологический вопрос: как отличить ФП от не-ФП? Является функциональность свойством программы, языка или чего-то еще? Если программы — значит, есть набор свойств, который можно формально установить. Тогда нужен их полный перечень. Если языка — то снова необходимо четко определить набор свойств, которым гарантированно удовлетворяет любая программа, на этом языке написанная. Причем он должен быть общим для всех языков, которых мы относим к функциональным. Если у Вас такой перечень свойств-индикаторов имеется — предоставьте его, чтобы не возникало терминологических споров. Одной «чистоты» для этих целей явно мало, тогда уж лучше делить на «чистые» и «нечистые» языки, и на таком разделении сильно далеко не уедешь. Ну чистый язык. И что? Какие еще полезные свойства программы из этого следуют? Деление по используемой модели вычислений намного более конструктивно, поскольку в качестве следствий охватывает все то, что мы естественно воспринимаем как атрибуты функциональной парадигмы. И если это принять, то если Idris не основан на данной модели вычислений, то в указанном смысле считаться функциональным не может. Хотя синтаксически вполне может быть намного ближе к типичным функциональным языкам, чем к типичным императивным.

Это такая же зыбкая почва, как "Что такое ООП". Вот все сходятся, что джава — ооп, а допустим си — нет. Почему? Вон, куча примеров, как люди делают структуры, руками таскают указатели на vtable, вот вам все три кита, под которыми обычно ООП продают. Или это что-то еще?..


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


Точно так же можно сказать про ФП. Можно ли заэнкодить в тайпскрипте ХКТ, сделать там монады-аппликативы? Да запросто. Будет ли этим удобно пользоваться? Не факт… Вопрос: является ли TS функциональным?

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

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


Много разных вариантов.

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

Вроде автор ООП не считает, что Java ООП :)

НЛО прилетело и опубликовало эту надпись здесь

Разумеется, класс доказуемо завершимых функций также расширяется, как минимум на одну функцию, и она есть тут в статье :-)

НЛО прилетело и опубликовало эту надпись здесь

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

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

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


У меня имелся опыт, связанный с обучением функциональному подходу. Обычно трудность в том, чтобы заставить перестать проводить ложные параллели со знакомыми конструкциями из структурного и ООП-программирования и переносить привычные подходы на функциональные программы. Похожая ситуация, только с обратным знаком, может (на мой взгляд) иметь место в случае вашего поста про монады: перенос абстракций в парадигму, где им не место. Как я упоминал в статье, монады — естественное для ФП образование, призванное решить ряд его естественных же внутренних проблем. Для языков типа шарпа они выглядят искусственным конструктом с неочевидными преимуществами. Так что само появления кода, реализующего монады и функторы, я воспринял неоднозначно.

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

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

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

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


Про императивщину и ФПшность неплохо у Бартоша написано, где он сравнивает два различных взгляда на программу.

Даже если таких людей пара человек — это стоящая работа
Все ровно до того момента, пока программа не сталкивается с внешним миром. А вот он внезапно такой не хороший не хочет быть чистым. В итоге сайд-эффекты все же от этого присутствуют и много усилий в функциональном программировании затрачивается на их минимизацию.
Проблема взаимодействия с внешним миром концептуально давно решена и привела к появлению монады IO
Это всего лишь метод как спрятать проблему под коврик :) От того что она «решена» она никуда не девается. Точно так же работает ORM. Там тоже проблема «решена».
Так и есть — это способ скрыть проблему за стеной абстракции. Программирование сверху донизу забито таким способами решения проблем.
Просто про нее надо помнить. Как и в случае работы с ORM. Но это справедливо для всех абстракций. Иначе можно выстрелить себе в ногу весьма вычурным способом.

Какой коврик? По-сути ФП программы накладывают одно ограничение "всё взаимодействие с внешним миром должно быть асинхронным". Я бы не сказал что это дофига сложное ограничение, более того, на практике у меня вон в программе всё взаимодействие асинхронное: получение хттп запросов, запросы в БД, запросы в эластик, ...


Выходит, что и ограничений никаких нет.

Эммм. Что? Каким образом асинхронная обработка чего либо делает функции чистыми?

Если у вас нет способа достать результат "Грязно" (то есть функции Promise -> T или аналога), то вы не сможете написать программу, которая читает или пишет нечисто. У вас просто нет способа пронаблюдать эффект, вы можете только композировать через then что делать. когда действие сделано.

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

Вариант не так уж и плох главное за шторку куда отдаем и откуда принимаем не заглядывать :)

Верно. Именно поэтому в том же хаскелле функция которая приоткрывает шторку называется unsafePerformIO, и против неё целые линты есть.


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

Единственное но, это в обратку получаем что все взаимодействие только асинхронное, а это еще уметь готовить надо. Хотя в js так давно и ничо справляются

Не знаю как у вас, а у меня 90% взаимодействие и так асинхронное, потому что по сети: либо с другими сервисами, либо с базой, либо ещё с чем. То что вывод в консоль/чтение из неё становятся асинхронными, ну это вроде не так уж страшно, тем более что асинк-авейт давно везде есть, в хаскелле вообще больше 20 лет уже.

Из чего следует требование асинхронности?

Апи IO совпадает с Async, по сути Async это маркерный ньютайп для IO. Никаких других причин заводить отдельный тип я не нашел (в чате мне тоже не смогли ответить).


Это самая простая и прямая аналогия с ежедневным программированием. ИСЧХ человек выше сразу же уловил суть.


Такие дела)

Да, но с чего вы взяли что асинхронность — требование к IO?

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

Асинхронность предполагает, что мы примерно представляем порядок вычислений. В ленивой модели мы не знаем (гипотетически), когда часть кода, обернутая в IO, начнет выполняться — до или после момента, когда нам реально понадобится вычисленное значение. Из чего следует, что эти моменты должны быть разнесены по времени? Может, IO ведет себя подобно Async, но ведь это вроде как не обязательно.

"Часть кода, обернутая в IO" начинает выполняться как только этот IO вернули из main.

Э… простите, вернули куда?

Вернули в качестве возвращаемого значения.

Вы, видимо, имеете ввиду main :: IO () из хаскелля. Но это же не единственная возможная IO в программе?

Любая IO в программе либо так или иначе оказывается скомбинирована внутрь той, которая вернулась из main; либо оказывается забыта и не выполняется. Ну, ещё есть третий вариант — unsafePerformIO, но он на то и unsafe.

Конечно она скомбинирована внутрь main. У вас все используемые функции как-то скомбинированы — в этом смысл. Как это вам помогает понять где начнётся выполнение какой-то части конкретной функции в ленивых вычислениях?

Если написано main = foo >> bar — значит, будет выполнена сначала foo, потом bar.


Что здесь меняет ленивость?

Если написано main = foo >> bar — значит, будет выполнена сначала foo, потом bar.

С чего Вы решили?

С того, что смысл операции >> именно в этом.

И из какой части определения монады это следует?

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

Вообще-то бремя доказательства обычно ложится на того, кто делает утверждение. Есть основания утверждать, что приведённый код может быть интерпретирован таким и только таким образом?

Потому что доказывать нужно существование чего-то, а не отсутствие, так что бремя доказательства тут так не работает. Вопрос звучит как "есть ли основания утверждать что определение точки в пространстве такое?". То есть, это как-то считается интуитивно понятным, но доказательства что другого определения точки нет, я не знаю.


Впрочем, как уже выше я написал, контрпример быстро бы расставил всё по своим местам.

Правильно. Имеем утверждение
foo >> bar — значит, будет выполнена сначала foo, потом bar

Это определенно не аксиома, значит оно должно следовать из каких-то предпосылок. Я спрашиваю: из каких?

readFile «1.txt» >> writeFile «2.txt» «blabla»

Какой закон для монады помешает сначала записать 2.txt, а после прочитать 1.txt? Без шуток, может я действительно что-то забыл или не знал.
Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages.

https://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Monad.html#v:-62--62-


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

Определение по умолчанию
m >> k = m >>= \_ -> k
этого вроде как не гарантирует. Наверняка для IO в реализации хаскелля это так. Но я пока не вижу формальных гарантий именно такого поведения.

Вообще-то гарантирует. Потому что операция >>= тоже обладает нужным свойством.

Следует из того, что… ?

НЛО прилетело и опубликовало эту надпись здесь
Спасибо, такое объяснение вполне меня устроит. Но это ведь не единственная возможность реализации IO? И в принципе ничто не мешает произвести на свет версию, которая будет может первым вычислить второй операнд >>=, если аргументы независимы.

Мешает. Мешает тот факт, что такое IO никому нахрен не нужно.


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

Если аргументы независимы, то какая разница в каком порядке они будут вычислены? Это помешает разве что провести прямую параллель между IO и последовательным выполнением инструкций, но ввод-вывод-то тут причем?
Я уточню, а то дискуссия явно уходит не туда. В монаду IO обернут некий функционал. Он может включать в себя не только откровенно «грязные» действия типа записи в базу данных. Естественно, при компиляции для таких действий трудно или даже невозможно определить, являются ли они взаимозависимыми. Обзовем это (a >> b). Но допустим также, что перед тем, как выполнить второе «грязное» действие в b имеется вполне себе чистый код c, то есть (b = f( c )). Тогда раз c точно не зависит от a, ничто не мешает вычислить его значение до a и только потом довычислить b.
НЛО прилетело и опубликовало эту надпись здесь
Лучше сам отвечу )
Мешает, конечно, принятое правило редукции. У меня что-то из головы вылетело, что в конкретной реализации аргументы получают значения все-таки не в произвольном порядке.

Нет, правило редукции тут как раз ни при чём. Аргументы foo и bar могут оказаться вычислены в любом порядке. Но вы исполняться они будут строго в том порядке, в котором их склеила операция >>.

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

НЛО прилетело и опубликовало эту надпись здесь

Тут всё-таки обсуждается монада IO, а не произвольная.


Впрочем, единственное отличие эффекта "a*2" от writeFile — в том, что вычисление a*2 можно выкинуть (что и произошло), а writeFile выкинуть нельзя.

Почему? Может ради него всё и писалось.
НЛО прилетело и опубликовало эту надпись здесь
Изолировать проблему в отдельной хорошо видимой части программы не означает спрятать ее под коврик. Ну никаким боком. Вы просто будете знать (в том числе на этапе компиляции), где у вас чистые функции, а где есть IO. И нет, это в общем-то совсем не много усилий. В других подходах они тоже будут, просто выражены иначе.
Я немного про другое. Нужно понимать что где-то всегда будут грязные функции и от них не избавиться.
Я именно об этом. IO позволяет их изолировать, и точно знать, где они. Вообще чистота — это давно уже не про то что нечистых функций нет, а про то, что мы точно знаем, где они.
К сожалению, история гораздо более сложная, потому что вызов функции — это тоже эффект. И seq — это эффект. И unsafeRunIO — тоже эффект. И появились они не из вредности, а потому что нужно на практике. Это я к тому, что не следует идеализировать. Haskell форсирует, конечно, дисциплину, но точности это не добавляет, потому что в библиотеках может быть всякое.
НЛО прилетело и опубликовало эту надпись здесь
Только если у вас есть ⊥. Но хаскелисты прикидываются, что у них его нет.

А при чём тут ⊥?

НЛО прилетело и опубликовало эту надпись здесь

Даже если бы в языке не было боттомов — seq всё равно следовало бы оставить, как один из инструментов оптимизации. Именно из-за его влияния на производительность, которая почему-то не всегда следует этим definitionally equivalent...

НЛО прилетело и опубликовало эту надпись здесь

seq дает наблюдаемую разницу только на боттомах. Собственно, про это есть в вики.

Вот разница между трэшингом и не-трэшингом во время исполнения программы она наблюдаемая или нет? Вопрос риторический, конечно. Но! Речь идёт именно о математической семантике seq. В математической модели языка её необходимо выражать, как эффект. Поэтому всякие разные категорные модели Haskell разваливаются.
Зависит от того, чем мы считаем beta-редукцию. Если она реализована, как операция, через изменение окружения и eval, а в языках программирования так и делается, то это эффект. Вот здесь вычитал

arxiv.org/pdf/1807.05923.pdf
НЛО прилетело и опубликовало эту надпись здесь

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


С внешним миром, внезапно, можно работать чисто. Зачем нужны по-вашему всякие хаскелли и скалы, если в них (если взять вашу точку зрения за истину) нельзя даже ответ, что оно посчитало, вывести на экран?

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

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

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

Ну, вывод в консоль там действительно нетривиальный. Впрочем, он никогда не проектировался под IO, его задача именно что задать правила и базу знаний, и получать результаты. Для своих задач вроде солвинга он более чем хорош. Использовать его как язык общего назначения — наверное, не стоит.

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

Операция тайпчекинга системы типов Rust — это достаточно полезная задача солвинга, не требующая взаимодействия с пользователем?

Раз уж у нас публичный «камминг аут» на тему ФП, то присоединюсь к нему.

ФП в «дельта-окрестность моего информационного поля» попало с год назад, в основном благодаря статьям на хабре / ЖЖ. Изучать его я начал 2.5 месяца назад на новогодние каникулы (и поставленных целей, на мой взгляд добился, хотя впереди ещё «типы в языках программирования» и «чисто функциональные структуры данных»).
Забавное уточнение: попытка изучить ФП пол-года назад в мультипорадигменном Python у меня провалилась (что такое map\recude\filter я и так знал, а всё остальное в Python проще написать… мультипарадигменно (*)).

По результатам скажу так: если вы не знаете ФП то не читайте (с целью получить общее представление) статьи о нём в популярных источниках, — скорее всего получите превратное понимание.
Лучше возьмите любую книгу (или курс на любом ресурсе) и изучите\пройдите до конца. Мне понравилось «изучай Хаскель во имя добра» (например 14 занимательных эссе о Хаскель куда хуже легли лично на моё понимание).

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

*) Тут есть интересный вопрос: а есть ли ФП для начинающего (вопрос в том, чтобы изучить) за пределами Хаскеля? Я его не обнаружил и сложности, на мой взгляд тут вот в чём:
— по более идеоматичным ФП-языкам (agda, idris) на порядок сложнее найти вводные материалы
— для мультипарадигменных сложно выбрать хороший курс (не вида: how to use map\filter\recude) и если курс найден, дойти его до конца, искуственно ограничивая себя функциональным подходом в мультипарадигменном языке.

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


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


А курсы по "фп в языке %мейнстрим_ланг_нейм%" мне тоже не очень понравились — много мусора, толку не очень.

НЛО прилетело и опубликовало эту надпись здесь
Ну и по идрису, и по агде есть весьма годные книжки (Type-driven development with Idris, Verified functional programming in Agda, дальше чуть хардкорнее Programming language foundations in Agda). А что плагин не обновлялся — ну, идрис-плагин для вима тоже примерно столько же не обновлялся, но зачем, если он просто работает?

У кого работет? У меня например — нет, баг что не работает 3 года висит открытый. Так и живем.

НЛО прилетело и опубликовало эту надпись здесь

У меня есть некая претензия к фанатам "ленивых вычислений".
… Если у вас вычисления, то их надо делать. Иначе, зачем их написали в программе?


Если у вас в программе вычисления, которые появляются как результат пересечения двух абстракций и их быть не может — для этого есть zero cost abstractions и такое должно вычищаться компилятором как dead code.


Если у вас вычисления определяются внешними событиями (скроллингом пользователя, прерываниями, etc), то вы можете откладывать вычисления на момент происхождения события без использования ленивых вычислений (event driven, futures и т.д.)


Для меня ленивые вычисления — это такой оверкоммит по процессору. Типа, нам тут сложно разобраться что считаться будет, что нет, так что мы обещаем что считать будем всё, но на самом деле будем считать только то, что понадобится. А если понадобится считать до конца (например, узнать len() у ленивого бесконечного списка), то ой. Прийдёт местный полицай и устроит bottom (аналог oom'а для вычислений).

НЛО прилетело и опубликовало эту надпись здесь
Парсеры, ведь, энергичные, если я верно помню, они делаются через state applicative и монаду. То есть, вроде как, ленивость особого значения не имеет, потому что в любом случае создаются thunk-и, которые послойно разворачиваются.
НЛО прилетело и опубликовало эту надпись здесь
Так, ведь, Alternative тоже через state делается: вызываем одну функцию, если результат не подходит, вызываем другую. Это явно прописывается. Недавно делал такое упражнение, память свежа. Комбинаторы же создают state-значения, то есть функции преобразования state, то есть, они в любом случае уже экранированы, как лямбды.
НЛО прилетело и опубликовало эту надпись здесь
У меня есть некая претензия к фанатам "ленивых вычислений".
… Если у вас вычисления, то их надо делать. Иначе, зачем их написали в программе?

если у вас написано if (a == 3 && b == 5) и a оказалось равно трем, зачем вы писали b == 5? Ведь оператор && в си-подобных языках как раз ленивый, как и ||.




Если обобщить это с булевых операций на все остальные, и получим хаскельную ленивость. Тем не менее, про булевы оператор никто не спрашивает "а зачем вы их пишете", почему тогда в общем случае такой вопрос возникает?




Лень помогает писать эффективные рекурсивные данные, бесконечные данные, или управлять флоу снаружи: все эти императивный while/break/early return в ленивом языке появляются просто сами по себе, без необходимости заранее об этом думать.

На самом деле лень — это оптимизация best case. При этом, когда приходит worst case, мы получаем tail latency от которой индустрия уже лет 5 пытается избавиться и не может. Это когда быстрый код почему-то иногда делает медленно.


Пример (того же уровня): if (fast_ok() && slow_second_guess()), и у вас fast_ok() выполняется в 99.995% случаев за 30μs, а slow_second_guess вычисляется 20 секунд, и вы пытаетесь понять, что не так.

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


const int myTreshold = 100;
var max = foos.Fold((acc, x) => Max(acc, x, myTreshold));

Так вот в в строгом языке мы будем проходить весь итератор (из миллиона допустим элементов), хотя из первых 10 мы упёрлись в 100 и результат уже дальше не поменяется.


Какой выход в энергичных языках? Ну, костылить такой же фолд, но "с вывертом", чтобы возвращать флаг, продолжать ли вычисления дальше или нет:


const int myTreshold = 100;
var max = foos.FoldEarlyReturn((acc, x) => 
 acc >= myTreshold ? (myTreshold, finished: true) : (Max(acc, x), finished: false));

В лениовм языке самая первая версия работает ожидаемым образом, и дублировать апи для "Ранних выходов" не надо.


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

Простите, но зачем итератор исчерпывать полностью? Обычный if решает проблему.

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


И когда у вас есть супер-хороший комбинатор MyUsefulCombinator, который всем хорош, но только он исчерпывает все элементы, то либо идти и писать руками (то, что за вас уже написано, в целом), или идти заводить issue "пожалуйста, сделайте такой же комбинатор, но другой". Как видите, в расте идут по обоим путям сразу.

Ну, кстати, в rust именно так. И break, и continue, и return — это выражения с типом ⊥ (!). Т.е. мы пишем выражение, в котором значение может быть return (break) и т.д.

Я к тому, что то что в расте требует двух функций (fold/try_fold), в ленивых языках является единственным foldr.

Пример (того же уровня): if (fast_ok() && slow_second_guess()), и у вас fast_ok() выполняется в 99.995% случаев за 30μs, а slow_second_guess вычисляется 20 секунд, и вы пытаетесь понять, что не так.

Вы что, пытаетесь сказать что если бы код работал за 20 секунд во всех случаях — было бы лучше?

Да. Тогда бы было известно, что это медленное место, которое бы починили.


Представьте себе, что slow_second_guess не тупит 20 секунд, а роняет программу. На тестах до этой ветки код не доходит и хорошо, а в продакшене раз в 0.05% случаев — роняет.


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

Вполне возможно, что это и без того известно, и именно потому проверка fask_ok() была в принципе добавлена.

Это как раз и вызывает tail latency.


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


Т.е. код, который есть, но 99.995% случаев не выполняется — это не очень хороший код. Его трудно проверить, в нём могут быть логические ошибки, или, ещё хуже, предположения, которые уже давно не правда (но никто код не менял, потому что он обычно не выполняется).


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

Скажите, а если я напишу вот так:


if (fask_ok()) {
    slow_second_guess();
}

Это ваш tail latency не вызовет?


Если нет, то в чём разница?
Если да, то почему вы не предлагаете запретить условные операторы?

Если вы так напишите, проблему быстро поймают (if fast_ok() будет часто).


В целом, если у вас будет 20 вложенных if'ов в функции, то её завернут за цикломатическую сложность (или пропустят — но тут уж sky is the limit). А вот ленивые вычисления этот if прячут.


Во, правильный аргумент: ленивые вычисления прячут if от программиста, скрывая истинную цикломатическую сложность кода.

Странный какой-то аргумент.


Цикломатическая сложность — это метрика, которая отражает сложность восприятия программы программистом.


Если эту самую сложность от программиста "успешно скрыли" — это всего лишь означает, что она перестала отражать что бы то ни было.

Смотрите, есть два вида скрытия.


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


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


Когда у вас if good() && bad() в неявном виде, это прячет сложность от того, кому её надо видеть. Вместо того, чтобы увидеть, что bad() будет вызван только если good() не удался, человек видит, что условие выполняется.


Это нарушение дзена (питона) — "явное лучше неявного".

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

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


class mydict(dict):
   def items():
       yield {'a': 1} # etc
       if not time.time() % 1337:
           repeat = False
           while repeat:
             try:
                 with file('/etc/foobar.conf') as f:
                      yield from yaml.load(f).items()
                      repeat = False
             except:
                 pass

И вот вы юзаете mydict, и он выглядит как mydict, крякает как mydict, только раз в иногда делает какую-то фигню.


Скажите, что я скрыл от программиста тут? Гигантский толстый wtf, который чёрти как отладить, и который спрятал сайд-эффекты в чистом коде.


Это пример плохого дизайна, в котором скрыта сложность (допустим, этот файл реально надо читать в тот момент, когда число секунд кратно 1337)/

Да ничего вы тут не скрыли. Тем более, что в приличном языке у вас items() возвращает IO (Map String Int), и можно сразу подумать, что программист который эту функцию писал не очень хорошо перед этим подумал.

А вас не смущает, что чтение файла, даже будь оно реализовано максимально явно, может отличаться по времени на порядки, в зависимости от того, было это первое обращение или повторное? Да и любой другой кеш представляет собой "if good() && bad() в неявном виде"
Это тоже "абстракция, которая скрывает то, что не надо скрывать"? Нужно ли программисту видеть всю подноготную в коде?

Ему этот if надо видеть. Если if'а нет, то это wtf для выстрела в ногу.

Нормальный, поддерживаемый код:


double absolute_difference(double a, double b) {
    if (a > b)
        return a - b;
    else
        return b - a;
}

Говнокод, страх и ужас, никогда так не делаете:


double absolute_difference(double a, double b) {
    return std::abs(a - b);
}
НЛО прилетело и опубликовало эту надпись здесь

… Я нет, а вот разработчики ядра — всё чаще и чаще. retpoline'ы, KPTI и т.д. Спектре и его производные оказались такой мерзостью, что на моём ноутбуке в /proc/cpuinfo есть вот такое:


bugs: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit

НЛО прилетело и опубликовало эту надпись здесь

А у меня есть сервер с аптаймом в 11 лет. Непатченное ядро 2.6.18.


У кого из нас длиннее и грязнее?

НЛО прилетело и опубликовало эту надпись здесь
Статья неплохая, но всё же присутствует однобокость. Качественный подход предполагает нечто более многостороннее. Вот примеры:
Функция fibonachi (а это именно функция) порождает бесконечный список чисел

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

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

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

Здесь лучше не приводить «тезисы», взятые из обсуждений одних «знатоков» с другими. Изначально ФП ориентировалось на лямбда-исчисление (по сути — прямая реализация), значит можно попробовать его определить именно через поддержку операций лямбда-исчисления. Но потом к нему добавили расширений, что несколько размыло определение. И хотя расширения логически вытекают из положенного в основу лямбда-исчисления, но всё же само лямбда-исчисление было ограничено (бесконечных теорий не бывает), а потому, не смотря на дальнейшие логические связи, возможно, стоит строить определение ФП именно на основе соответствия лямбда-исчислению. Но тогда определение станет непонятным большинству потенциальных пользователей продукта «язык программирования». Значит стоит выделить некую характерную часть лямбда-исчисления, как например в статье выделена частичная редукция, и как следует её «разжевать». Правда и тогда опять будут ссылки на то, что «из этого логически вытекает ленивость».

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

Да нет, если вам нужно изложить императивный алгоритм — берете и прямым текстом его излагает. Например, вот мутабельный инплейс квиксорт на этом вашем хаскелле, прям как на си:


qsort :: (V.Vector v a, Ord a) => v a -> v a
qsort = V.modify go where
    go xs | M.length xs < 2 = return ()
          | otherwise = do
            p <- M.read xs (M.length xs `div` 2)
            j <- M.unstablePartition (< p) xs
            let (l, pr) = M.splitAt j xs
            k <- M.unstablePartition (== p) pr
            go l; go $ M.drop k pr

Можно не включая мозг просто переписать алгоритм 1в1.

А дейкстру (обход графа в ширину) написать сможете?

Никогда не интересовался алгоритмами на графах, но вот например несколько реализаций, включая мутабельную: https://github.com/hcrudolph/haskell-dijkstra

А какую из тех реализаций вы считаете лучшей?
И какая реализация проще — на хаскеле или например C++?
rosettacode.org/wiki/Dijkstra%27s_algorithm#C.2B.2B
Как оцениваете ассимптотическую сложность алгоритма на хаскеле?
Интересует так сказать экспертное мнение

PS. Там по ссылке с C++ чуть ниже есть более «стильная» версия

Ну, за экспертным мнением это скорее к 0xd34df00d и подобным товарищам, я пока в прод массово на хаскелле не писал, у нас только небольшой скала сервис.


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


По строчкам кода если посчитать то одинаково выходит: и там и там 60, не считая импорты и комментарии.


Что до асимптотики то по крайней мере для мутабельного варианта на IORef она совпадает с таковой у С++.

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

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

И по ощущениям в хаскелле поболее 60 строчек. Там же еще объявлений типов дофига (хотя может это не очень придирка). Ну в любом случае, для меня 60 строчек на хаскелле ощущается как 500 строк на C++. Может с непривычки конечно. Но долго пришлось в алгоритм вникать, и то вроде вник только до степени «ну наверно это так», а C++ код просто берешь и читаешь

Вот вы кстати знали алгоритм дейкстры до этого момента? Если нет, насколько быстро удалось его понять чтением haskell и C++ кода?

За IO на каждый чих — ещё как надают. А вот какой-нибудь ST или что-то более специализированное уже можно использовать.

Еще посмотрел немного код, в хаскелле он не совсем оптимальный, а в некоторых местах даже, кхм, странный

replicateM_ (M.size graph) $ traverse arc ps vs

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

Ну в том варианте что я на гитхабе нашел — понять что-то трудно, и тут плюсовый код однозанчно выигрывает в читаемости. Ну и странный он, местами возможно не особо оптимальный. Но если посмотреть по вашей ссылке реализацию как там сделали, то она куда лучше, и мне её читать чутка проще. Хотя примерно так же, как и сишная. Во многом потому, что там буквально runST и всё то же самое :)




Прочитал внимательнее хаскель вариант, они буквально пишут


Translation of: C++
Translation of the C++ solution, and all the complexities are the same as in the C++ solution. In particular, we again use a self-balancing binary search tree (Data.Set) to implement the priority queue, which results in an optimal complexity.
Ух, проще не стало. Чую на полное понимание хоть какого алгоритма уйдет не один час.

В результате все равно все алгоритмы выглядят как не очень удачная попытка эмулировать императивное программирование функциональным

Это не эмуляция, а собственно эмбеддинг императивного кода в ФП язык. Для того ST и придумали.


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


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

Ну если вы и вправду с одинаковыми умозотратами читаете как C++ код, так и haskell (конкретно для этого алгоритма), то конечно браво
НЛО прилетело и опубликовало эту надпись здесь
Ну дело же тут не в C++. Можно открыть код на любом другом яп. Да и с C++ тут ничего сложного особо нету

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


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

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

Зачем для реализации этого алгоритма нужно знать монады, я вообще не понимаю (точнее как раз наоборот, понимаю, но в этом и проблема)

В императивном яп ты просто садишься и пишешь. В ФП ты «творишь»

> крутой паттерн какой-нибудь придумали
А в фп же нету паттернов :)
НЛО прилетело и опубликовало эту надпись здесь
Дело не столько в синтаксисе, сколько в количестве мыслетоплива, которое нужно потратить для решения данной задачи.

Да сколько мыслетоплива? Тут буквально 1в1 написано то же самое, плюсовику с опытом дай, скажи "вместо присваивания пиши <-, в конце не забудь написать freeze. В начале напиши магическую фразу runST". Ну чутка вербознее чем плюсы, но это ведь понятно, потому что мутабельность в языке немного сбоку прикручена. Тем более что такое делать надо нечасто — многие вещи и в чистом коде отлично выражаются без оверхеда (как 0xd34df00d показывает в своих статьях), а это фоллбек для случая, когда чистого подходящего алгоритма не нашлось + нам не пофиг на перфоманс в этом месте.


Вербозно? Да, ощутимо больше писать чем в коде на плюсах. Стоит ли того? Чтобы писать только такой алгоритм — конечно нет, лучше взять более подходящий язык. Но если у вас 99% остальной программы написано в "нормальном" стиле, то ради них написать с некоторым количеством церемоний и плясок вокруг мутабельности — наверное нормально.


А в фп же нету паттернов :)

Ну как нет — конечно есть)
https://youtu.be/kR4mCDIHrac

Тем более что такое делать надо нечасто — многие вещи и в чистом коде отлично выражаются без оверхеда

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

Ну что поделать, чтобы учиться — надо думать. Зато когда выучился — можно пользоваться тем, что знаешь. И мне кажется, что потратить на какие-нибудь монадки или стандартные комбинаторы траверсаблов/фолдаблов пару дней стоит того, чтобы потом 20+ лет карьерной жизни пользоваться тем, что это знание даёт. Почему-то не все с этим согласны, ну да ладно.

И мне кажется, что потратить на какие-нибудь монадки или стандартные комбинаторы траверсаблов/фолдаблов пару дней стоит того

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

Бегло нет, но чтобы понимать, что написано — вполне. Особенно когда функция называется filterM/traverse, ане fooBarYouWontKnowMyPurpose

А творческая работа по изучению монад, do notation и прочего использованного в вашем коде? Ну и само введение поддержки do notation на уровне компилятора есть прямой путь к императивному стилю, но в функциональном языке.

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

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

Все-таки нет, это функция, у которой нет аргументов.
Здесь неопытнй читатель наверняка подумает...

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

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

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

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

Определение «естественности образа» опять же получится размытым, поэтому и разделение концепций не выйдет.

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

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

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

Хотя если придираться к определениям, то в данном случае, согласен, не присвоение, а определение константы с указанием её значения, вычисляемого функцией.
И в этой строчке она не вызывается.

Ну ладно, не в этой строчке. Так вас устроит? Или вы считаете, что функции вообще не вызываются?
В ведь понимаете разницу между константой и функцией без аргументов?
Нет, присвоение — это конкретная операция, предполагающая изменение внутреннего состояния. В «чистом» языке запись «g = f(x)» означает, что любое вхождение g может быть заменено на f(x). Но это не значит, что даже если x — конкретная константа, то каждое вхождение g будет заранее заменено на эту константу. Это возможно, но не обязательно.
Или вы считаете, что функции вообще не вызываются

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

Понимаю не совсем так, как вы.

Мы декларируем некий идентификатор и указываем, что он равен значению, возвращаемому функцией. Наверняка вы читали статейки на тему «eqational reasoning», там как раз именно с такой точки зрения всё рассматривается. И выходит очень логично и очень математично. Мне тоже нравится такой подход.

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

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

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

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

Разумеется я этого не предлагаю. Это просто наверное самый простой пример того, как это может работать. Более сложная конструкция не получается, поскольку вас, программиста, никто не заставляет писать программу в виде одного сплошного выражения. Для этого как раз мы и вводим вспомогательные сущности, в данном случае функции, а для их использования «Мы декларируем некий идентификатор». Вопрос не в том, как Вы записываете программу, а как она воспринимается исполнителем.
абстракция «частичного вычисления» внутри компилятора применима редко из-за сложности такого вычисления

… Хаскелль?
а потому можно ошибиться, если начинать продумывать, до какого момента что-то там частично вычислено

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

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

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

Мне не до фонаря. Я предпочитаю понимать, именно императивно, что там происходит. Потому что если понимать поверхностно — всегда найдётся случай, когда «понимание» оказывается непониманием.
Константа, показывающая пополняемый список… мне кажется, все таки употреблять термин «константа» (лат. constans, родительный падеж constantis — постоянный, неизменный) в таком смысле не правильно. В таком случае main в хаскелле тоже константа. То есть любая программа — константа.
Но ведь и ваше «императивное» понимание в определенной мере иллюзорно — вы не знаете, в каком порядке будут выполнены описанные действия после компиляции Вашей программы. Вы, говоря условно, можете быть уверенным только в том, что если результат действия b зависит от результата действия a, то a будет выполнено раньше b. То есть что компилятор не изменит вашу программу сущностно. В ленивых языках примерно та же ситуация, с тем отличием, что у Вас изначально нет иллюзии о том, что Вы располагаете сведениями о порядке вычислений.
При программировании в императивном стиле тоже сколько угодно случаев, когда понимание того, как сделать правильно/эффективно подводит. Если вы имеете ввиду, что для работы с языком нужно иметь понимание того, как в целом должна отработать написанная программа — то здесь я полностью согласен.
мне кажется, все таки употреблять термин «константа» (лат. constans, родительный падеж constantis — постоянный, неизменный) в таком смысле не правильно. В таком случае main в хаскелле тоже константа. То есть любая программа — константа.

Main общается с монадами, которые прячут внешние эффекты, а значит main не может быть константой из-за этих самых эффектов.

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

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

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

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

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

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

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

Ну и если посмотреть на других, то по ним видно, что оптимизацию запросов SQL осиливают далеко не все, что означает и в данном случае наличие заметной сложности. Хотя монады из тех, кто хорошо освоил SQL, так же понимает меньшинство. Правда моя статистика, понятно, субъективна. Но можно спросить на любом форуме по базам данных — кто из вас вообще знает про хаскель? А кто понимает вот этот код… и привести что-то с монадами. Ответ, мне кажется, будет соответствовать моей статистике.
Абстракции бывают разные. Абстракция SQL, как минимум для меня, очень понятна и я хорошо понимаю, как она ложится на императивно выражаемые алгоритмы. А вот абстракции из хаскеля я не все до такой степени понимаю. Возможно, это следствие отсутствия длительного периода использования хаскеля, ведь я с ним познакомился, поигрался, да и забыл о нём.

Каким образом она понятно ложиться на императивщину? У нас написано SELECT * FROM A JOIN B ON A.Id = B.EntityId, а там то ли цикл, то ли хэшмапа создалась, то ли сортировка началась. А может и еще что-то, при чем один и тот же код может по-разному работать даже на одной и той же версии БД, просто на основании статистики, не говоря о том, что можно накатить минорный патч и запрос совсем по-другому будет работать. Если это "хорошо ложиться на императивщину" — то хаскель с его монадами и подавно.


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

Не знаю, что там штурмовать: сказали в курсе, что есть функция a -> m b, и если у тебя есть f a то можешь получить f b. Показали пример со списком, опциональным значением и футурой, всё стало понятно. После пары лет работы в сишарпе с LINQ у меня возникло ровно 0 сложностей с этим. Как и у среднестатистического джава-разработчика, который работал со стримами, и пользовался flatMap и thenCompose.


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


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

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


Правда моя статистика, понятно, субъективна. Но можно спросить на любом форуме по базам данных — кто из вас вообще знает про хаскель? А кто понимает вот этот код… и привести что-то с монадами. Ответ, мне кажется, будет соответствовать моей статистике.

И что это докажет? Что люди не знающие хаскелля не могу читать хаскель? А можно показать людям не знающих плюсов — код на плюсах. А не знающим эрланга — на эрланге. Не знающим J — на J…

У нас написано SELECT * FROM A JOIN B ON A.Id = B.EntityId, а там то ли цикл, то ли хэшмапа создалась, то ли сортировка началась.

Цикл или сортировка — внутренние сложности алгоритма нахождения соответствия записей из А и В. Абстрактный же алгоритм очень прост. Но если мне надо, то я легко найду внутренние детали реализации. Вот в этом простота.
Не знаю, что там штурмовать: сказали в курсе, что есть функция a -> m b, и если у тебя есть f a то можешь получить f b. Показали пример со списком, опциональным значением и футурой, всё стало понятно.

У меня был очень простой вопрос — а зачем так? Вы же целую статью на эту тему написали, объясняя зачем. И там много комментариев со спорами.

Ну а поверхностно понять, что «на входе Х, на выходе У» — это конечно легко.
Когда нужно — быстренько выучивают все, что надо

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

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

Если это считать деталями алгоритма, то любой фпшный fmap это тоже "детали алгоритма", которые можно так же посмотреть.


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


У меня был очень простой вопрос — а зачем так? Вы же целую статью на эту тему написали, объясняя зачем. И там много комментариев со спорами.

Ну, иногда нужно. Зачем банда четырёх целую книжку с паттернами написали? И в обсуждении этой книжки споры про то, Как разные паттерны с SOLID/KISS и законом Деметры дружат.


В случае с БД — да, там всё просто. А в случае с хаскелем — я бы так не сказал.

Не вижу принципиальной разницы. Смотреть план запроса или в вывод гхц. Знать про все 547 видов индексов, разные режимы блокировок, сплиты страниц и всё остальное или знать про 547 монад, режимы жадного и ленивого выполнения, и всё остальное.


Докажет разницу в сложности. Большинство знающих хаскель легко разберутся с SQL, но большинству знающих SQL придётся нелегко, когда нужно будет разбираться с хаскелем.

Это не так.

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

А где вы смотрите детали? По БД есть очень хорошая документация, а вот по хаскелю — надо всё искать (если что-то сложнее основ). Вплоть до изучения кода компилятора.

Как работает ленивость? Автор статьи упомянул ленивое вычисление списка чисел Фибоначчи — как этот список получается (императивно)? Где искать?
Не вижу принципиальной разницы. Смотреть план запроса или в вывод гхц.

Но в обычной программе (императивной) вообще нет необходимости смотреть какие-то планы/выводы. Точнее в большинстве случаев достаточно просто подумать.
А где вы смотрите детали? По БД есть очень хорошая документация, а вот по хаскелю — надо всё искать (если что-то сложнее основ). Вплоть до изучения кода компилятора.

А где вы смотрите детали? По хаскеллю есть очень хорошая документация, а вот по БД — надо всё искать (если что-то сложнее основ). Вплоть до изучения кода компилятора.


Как работает ленивость? Автор статьи упомянул ленивое вычисление списка чисел Фибоначчи — как этот список получается (императивно)? Где искать?

Например на официальной вики? Про то как работает непосредственно вычисление можно почитать по ссылке WFNF.


Но в обычной программе (императивной) вообще нет необходимости смотреть какие-то планы/выводы. Точнее в большинстве случаев достаточно просто подумать.

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

А где вы смотрите детали?

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

Посмотрел, но там же стандартные пояснения про тривиальные случаи. Это и так понятно. Мне же хотелось понять, как конкретно увязаны неизменность списка с ленивостью его заполнения. Из стандартного объяснения про сохранение вычисления и его реальную обработку «когда надо» непонятно как же обрабатывается ленивый список из функции Фибоначчи. То есть «реально надо», когда я прошу дать мне первый элемент, а потом «реально надо», когда прошу дать второй, и т.д. На котором шаге что считается? Где неизменность списка? Где смысл функции, возвращающей всегда один и тот же результат при неизменных аргументах? Как это всё увязывается в реальности?
В императивной программе точно так же надо смотреть на ассемблерные листинги и что там получилось.

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

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

А вот хаскель компилируется в машинный код и его тут недавно (была статья) декомпилировали именно для понимания, а что же он там делает.
НЛО прилетело и опубликовало эту надпись здесь
Я его вынес вне цикла, и код ускорился условно с 260 мс до 255 мс.

То есть у вас претензии к потере 5мс из 260? Два процента. Это явно излишняя оптимизация (в большинстве случаев). Тратить время на это нехорошо, потому что есть более важные задачи (обычно).

А по ассемблеру — если уж совсем по мелочам важно всё отследить, то лучше ассемблерные вставки делать. Там контроль полный и опять ненужно ничего декомпилировать.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Пришлось консультироваться с DBA.

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

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

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

На сколько я помню — везде вставляется признак strict mode.
Они не могут поменять смысл программы.

Попробуйте сделать ленивый список и потом использовать strict mode. Я думаю случится что-то страшное (типа память кончится).

Да, страшное случится. Но смысл программы всё равно не поменяется.

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

Если бы это действительно было так, то в Base не было бы отдельно foldl и foldl'.

В ведь понимаете разницу между константой и функцией без аргументов?

Я вот лично не понимаю. Расскажите, пожалуйста, а то у нас пару недель назад было обсуждение в чате, мы не нашли никаких отличий. Для аналогии: википедия по хаскеллю говорит что Well-formed types (то есть инты там и стринги) можно рассматривать как нульарные тайп конструкторы.

Разница всё-таки есть, хоть и ненаблюдаема средствами языка. Производительность, как всегда, портит красивые модели :-)


К примеру, вот в этой программе


fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

константный список будет вычисляться за O(1) на каждом шаге, а вот "функция 0 аргументов" экспоненциально взорвется (если только оптимизатор не догадается заменить её на константу).

Не взорвётся при call by need стратегии.

А что помешает?

В смысле что? При call by need fibs вычислится только раз. Объявление переменной это же связывание, а связанный терм при call by need вычисляется лишь однажды.

Только если это константа, а не "функция 0 аргументов".

Только если это константа, а не "функция 0 аргументов".

С чего вдруг?


Вообще, довольно бессмысленный спор, потому что:


  1. у нас нет ни одного ленивого языка в котором такую тормозящую "функцию нуля аргументов" можно было бы написать
  2. поскольку семантически в ленивом ЯП ф-я нуля аргументов от константы не отличается (с чем вы согласны как я понимаю), то любой реальный компилятор написанный реальными людьми не будет такие ф-и и константы различать, потому что никто не станет делать "специально тормозящий" компилятор

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


И да, оптимизация бывает не только по времени работы, но и по памяти. Во втором случае оптимизировать функцию 0 аргументов до константы некорректно, а время работы функции fibs становится проблемой программиста.

Вы же это не мне отвечали?

В ведь понимаете разницу между константой и функцией без аргументов?

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

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

Это неверно.

А обоснование прилагается?
А обоснование прилагается?

Охренеть! Оно еще имеет наглость обоснования требовать!


Окей, так уж и быть, дам подсказку, вот есть функции g y = g y, f x y = g x, тогда если я выполню f x, ака вызову partial, то что там при вызове произойдет, вычисление чего, а? Небось g x аж до константы свернется?

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

Речь не об утрировании и упрощении, а о том, что вы написали полную чушь. Не какую-то неточность, а просто бред, не имеющий никакого отношения к действительности. Непонятно откуда вообще вы вытащили это утверждение, его же еще придумать надо было!


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

Ну так вы попытайтесь ответить на вопрос:
"вот есть функции g y = g y, f x y = g x, тогда если я выполню f x, ака вызову partial, то что там при вызове произойдет, вычисление чего, а?"


и поймете


и как это влияет на полученные выводы.

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

4.2 What is partial evaluation?
Given a program and its input data, an interpreter can execute the program producing a nal answer. Given a program and only part of this program's input data,
a program specializer will attempt to execute the given program as far as possible
yielding as result a residual program that will perform the rest of the computation
when the rest of the input data is supplied.

Источник: Neil D. Jones, Carsten K. Gomard, Peter Sestoft. Partial Evaluation and Automatic Program Generation — Prentice-Hall, Inc., 1993, ISBN: 0-13-020249-5

Эти ребята тоже, судя по всего, хреново разбираются в вопросе частичного применения.

Ох лол, да у тебя в голове ещё большая каша чем я думал!


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


А у самого скомороха остаётся на выбор два варианта:


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

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

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

А если сказать так — "при вызове partial может быть вычислено всё то, для чего достаточно только первого аргумента"? (Но может быть и не вычислено, если компилятор/оптимизатор не сможет доказать это "достаточно")

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

Противопоставление «императивного подхода» функциональному, оно уже «как бы намекает».

Канонически (скажем так) императивному «подходу» противопоставляется *декларативный*… который, внезапно, не ограничивается ФП. И, какая-нибудь, «чистота» — следствие как раз декларативности, а не функциональности :-)

Возвращаясь к «иной модели вычислений»… Можно таки чуть более развернуто?
Можно. В данном случае да, иной — это «ленивой», поскольку из ленивости вытекают многие из тех свойств, которые присущи функциональным языкам и воспринимаются как своеобразные маркеры «функциональности». Рассматривать в контексте ФП только ленивые языки, разумеется, является очень большим упрощением, поскольку ФП в современном понимании включает в себя далеко не только их. Но охватить всё я не в состоянии. Да и цель была другая. Я ставил целью показать, что суть функционального подхода не в том, что берется раз, два, три, десять свойств, которые должны соблюдаться в языке программирования, а берется некоторая теоретическая основа — система типизации, модель вычислений и прочее, на их основе создается язык, который исходя из этих основ обладает рядом «функциональных» свойств.

Ленивые вычисления имеют к ФП непосредственное отношение. Если вам известен императивный язык, целиком и полностью основанный на концепции ленивых вычислений — буду рад, если Вы расширите мой кругозор.
«чистота» — следствие как раз декларативности

Логическое программирование декларативно, но Пролог «чистоты» не предполагает. Ваше утверждение требует доказательства. Я, в свою очередь, продемонстрировал, что чистота — необходимое требование в случае, когда предполагается использование ленивых вычислений.
В данном случае да, иной — это «ленивой», поскольку из ленивости вытекают многие из тех свойств, которые присущи функциональным языкам и воспринимаются как своеобразные маркеры «функциональности».
Вообще-то говоря, с точностью до наоборот. В том смысле, что «многие из тех свойств, которые присущи функциональным языкам и воспринимаются как своеобразные маркеры» замечательно укладываются в «ленивость вычислений»… и с этим-то никто не спорит. Важно то, что все эти «свойства» и «маркеры» ей — т.е. «ленивостью» вычислений — отнюдь не определяются. Вы, грубо говоря, на полном серьезе утверждаете ФЯП => «ленивые вычисления». Что, мягко говоря, не так.

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

А… ну если в этом смысле, тогда вам нужно как-то переформулировать свое утверждение в «ленивые вычисления» => ФЯП. :-) И это утверждение, кстати, тоже дискуссионно. О чем постараюсь чуть ниже…

… но Пролог «чистоты» не предполагает.
Да ладно?! «Чистоты» он «не предполагает» только в, скажем так, «процедурной» своей части. А она, как раз не декларативна.

Но давайте таки «про чистоту» :-) Императивность — если смотреть на неё со стороны модели вычислений — есть не более чем *строго определенный* порядок вычислений. И «императивный поход» — это, собственно, и есть описание этого порядка. Декларативность, соответственно, лишь означает *произвольный* порядок вычислений. Вот этот самый «произвольный порядок вычислений» (а не какая-то гипотетическая «функциональность»), и выводит нас на т.н. «чистоту» aka «ссылочная прозрачность». Без которой произвольный порядок вычислений попросту не возможен *в принципе*.

А теперь про то, как «ленивость» — в смысле, call-by-need — связана с «чистотой» и, соответственно, с каким-либо порядком вычислений. А никак она с этим не связана :-) В том смысле, что «ленивость» — вообще говоря — не означает *произвольности* (она про другое вообще). А если нет требования произвольности порядка вычислений, то, следовательно, не требуется и какой-либо «чистоты».

Другое дело, что да… есть некоторые «плюшки», которые «ленивость» вычислений дает в, скажем так, декларативном окружении. Причем, не сама по себе. А вот в императивном этих «плюшек» уже нет. Но все это следствия как раз императивности/декларативности, а не «ленивости» самой по себе.

Собственно, то, что мы — в данный момент — не наблюдаем императивных ЯП «целиком и полностью основанных на концепции ленивых вычислений», это как раз про наличие отсутствия «плюшек». А не про то, что call-by-need не возможен *в принципе* при «императивном подходе».

Возвращаясь, опять же, к «иной модели вычислений»… ФЯП это действительно про «иную» модель вычислений. И в первую очередь, это модель с *произвольным* порядком вычислений. «Ленивость» — как и «энергичность» — этих самых вычислений — она, в лучшем случае, вторична.
Вы, грубо говоря, на полном серьезе утверждаете ФЯП => «ленивые вычисления»

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

Ладно, принимается. Исключим из рассмотрения часть пролога для работы с базой утверждений. Но утверждение «декларативность» => «чистота» все еще требует доказательства. Что предполагает наличия определения термина «декларативность». Мы пока с ФП толком не разобрались.
Но давайте таки «про чистоту»

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

Зависит от определения. Если использовать правило «call-by-need» — то это уже в достаточной мере определяет порядок вычислений. Я использовал термин в более широком смысле: ленивый порядок — любой, не предполагающий вычисление значения всех аргументов функции до вычисления самой функции.
call-by-need возможен *в принципе* при «императивном подходе». То есть может быть в нем реализован. Он просто не является прямым следствием «императивного подхода».
Но все это следствия как раз императивности/декларативности, а не «ленивости» самой по себе.

Опять же, такое утверждение требует наличия строгого определения декларативности, из которого можно было бы вывести эти «плюшки».
И в первую очередь, это модель с *произвольным* порядком вычислений.

Полностью произвольного порядка вычислений быть не может, вы так алгоритм, используемый для вычисления определить не сможете. Ленивость и энергичность — часть модели вычислений. В некоторых случаях определяющая, в некоторых нет. О ее первичности, вторичности или n-ричности в общем случае, думаю, утверждать не стоит.
Но утверждение «декларативность» => «чистота» все еще требует доказательства. Что предполагает наличия определения термина «декларативность». Мы пока с ФП толком не разобрались.
?! Хорошо вам… я-то вообще не могу себе представить, как можно начать разбираться с ФП, не разобравшись *сначала* с декларативностью :-) И определение «декларативности» с т.з. вычислений я уже давал. Если вы с ним не согласны — ваше право… дайте другое :-)

Выражение «произвольный порядок вычислений» как минимум предполагает указание модели вычислений. Что я и сделал. И вывел чистоту как следствие этого самого произвольного порядка. В другой модели вычислений это утверждение снова потребует обоснования.
При всем моем уважении, я вот этого в статье не увидел вообще… хотя специально сейчас перечитал её ещё раз. Вы можете указать конкретно, где вы вывели «чистоту как следствие этого самого произвольного порядка»?

А я — ещё раз — попытаюсь объяснить что увидел я. Вы упомянули две модели вычислений (Тьюринга и Черча), и заявили, что во втором случае возможно «так называемое частичное вычисление», так — насколько я понимаю — вы «обозвали» т.н. non-strict evaluation. После чего объявили такое «частичное вычисление» — "«ленивой» моделью вычислений". Вот и все… и, лично с моей точки зрения, оба ваших утверждения не совсем корректны. Первое (про «частичное вычисление») потому что собственно к модели Черча оно не имеет прямого отношения, а из статьи складывается именно такое ощущение. Второе — потому что «ленивость» (как, собственно, и non-strict evaluation), это — как минимум — не модель… оно вообще про другое.

Зависит от определения. Если использовать правило «call-by-need» — то это уже в достаточной мере определяет порядок вычислений. Я использовал термин в более широком смысле: ленивый порядок — любой, не предполагающий вычисление значения всех аргументов функции до вычисления самой функции.
Угу… «от определения» :-) С чего вы вообще решили, что «не предполагающий вычисление значения всех аргументов функции» — это только к модели Черча относится?! Какие-нибудь short-circuit, call-by-name и т.п. evaluation они в модели Тьюринга нормально себе живут. И тоже «не предполагают» ничего такого. Да даже call-by-need, он не про *порядок*, а про «чистоту». Т.е. императивность тут нисколько не мешает. А вот отсутствие ссылочной прозрачности — очень даже. Т.е. — при условии соблюдения ссылочной прозрачности — call-by-need стратегия возможна и в модели Тьюринга. Вот только декларативной эта модель от этого не становится :-)

Полностью произвольного порядка вычислений быть не может, вы так алгоритм, используемый для вычисления определить не сможете.
Смотрите… отличие модели Черча от модели Тьюринга онож не в том, что тут у нас "β-редукция", а там её нет. Её отличие как раз в том, что от порядка выбора редексов результат *не зависит*. Таки да — это тот самый «произвольный порядок вычислений». Которого нет в модели Тьюринга. Внезапно выясняется, что алгоритм в модели Черча описывает все что угодно, но не *порядок* вычислений. Он — что называется — декларативный.

Ленивость и энергичность — часть модели вычислений. В некоторых случаях определяющая, в некоторых нет. О ее первичности, вторичности или n-ричности в общем случае, думаю, утверждать не стоит.
«Ленивость и энергичность» — это то, что — «по канону» — называется стратегия (в смысле, конкретный способ) вычислений. И применена эта стратегия может быть к любой модели… хоть к Тьюрингу, хоть к Черчу. То, что некоторые из конкретных стратегий для работы требуют некоторого, скажем так, «окружения» — ничего не меняет. И то, что выгода (те самые «плюшки») от применения этих стратегий в разных моделях разная — это, в рамках дискуссии, действительно «дело десятое», имхо. Принципиальна — сама возможность применения.

Т.е. посыл-то другой… Почему «ленивость» органично вписывается в ФП? Потому что там, «чистота» by design в силу декларативности. А не наоборот. И это вообще не означает, что не возможны «энергичные» ФЯП… как и «ленивые» ИЯП. Вот и все.
Я с ним согласен. Просто это не конструктивное определение, из него сложно обоснованно получить какие-нибудь следствия.

я вот этого в статье не увидел вообще

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

А я — ещё раз — попытаюсь объяснить что увидел я

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

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

отличие модели Черча от модели Тьюринга онож не в том..

Таки Вы снова правы. Я и говорил, что требование, например, «чистоты» есть следствие этой произвольности. А если порядок фиксированный и «тьюринговский», то это требование не является особо значимым. В конечном счете у нас все равно будет алгоритм вычисления выражения, в котором данный порядок будет каким-либо образом определен, но на требования к программе это не повлияет. В этом смысле да, программа декларативна, поскольку результат не зависит от способа вычислений.

Принципиальна — сама возможность применения

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

Если выводить все из декларативности… Давайте попробуем. Декларативные языки не определяют в явном виде порядок, в котором следует проводить вычисления. Гут. Соглашусь, что чистоту можно считать необходимым требованием декларативности. Функциональные языки декларативны. Логические языки декларативны. Языки запросов декларативны. Языки продукционных экспертных систем декларативны. Что это меняет? Отличий между ними больше, чем сходств. В ФП «чистота» приводит к появлению монад, в ЛП — к предикатом для работы с базой утверждений. Сама по себе «чистота» не приводит ни к каким значимым следствиям. А применение редукции в качестве механизма построения вычислительного процесса — приводит, применение метода резолюции в таком же качестве — приводит.
Вы пытаетесь исходить из того, что «ленивость» в том определении, которое применяется в практике программирования и действительно может быть отнесена и к функциональным, и к императивным языкам, «чистота», «функции высших порядков» и прочее — основы. Я же считаю их следствиями некоторых других основ, пусть даже следствиями столь значимыми, что они заслужили рассмотрение их в качестве самостоятельных сущностей.
И именно эти первопричинные основы достойны того, чтобы их изучать в рамках курсов функционального программирования.
Функциональные языки декларативны. Логические языки декларативны. Языки запросов декларативны. Языки продукционных экспертных систем декларативны. Что это меняет? Отличий между ними больше, чем сходств.
И? Всё их различие объясняется разными моделями. Согласитесь, что λ-исчисление и, какое-нибудь, исчисление предикатов — это таки «немножко разные» вещи. Хотя и то и другое — декларативно.

В ФП «чистота» приводит к появлению монад, в ЛП — к предикатом для работы с базой утверждений.
Да не приводит «чистота» к монадам. С чего вы вообще так думаете?! Монады — с т.з. вычислений — это вообще, если хотите, эдакое средство «линеаризации» этих самых вычислений. В смысле, способ их упорядочить. Причем, один из. И все… Если что к ним и «приводит», то это как раз «ленивость» вычислений… по той простой причине, что создает предпосылки к необходимости (порой) упорядочивать эту самую «ленивость».

Сама по себе «чистота» не приводит ни к каким значимым следствиям.
Само собой. Но «чистота» таки это необходимое условие для них.

А применение редукции в качестве механизма построения вычислительного процесса — приводит, ...
К каким, например? Я вот знаю один ФП, в котором не то, что «вычислительный процесс», в нем его квантование на редукциях основано. Но, в нем нет ни «ленивости», ни — тем более — модан и т.п. :-)

Вы пытаетесь исходить из того, что «ленивость» в том определении, которое применяется в практике программирования и действительно может быть отнесена и к функциональным, и к императивным языкам, «чистота», «функции высших порядков» и прочее — основы.
?! Это вы точно про меня? Я-то — как мне казалось — вам талдычу, что, как минимум, «чистота» — это не какие-то там «основы», а следствие декларативности. И моя, скажем так, претензия — она в том, что вы часть свойств приписываете ФП, выведя их *напрямую* из модели Черча. А они — *напрямую* — из неё не выводятся. Если хотите, из того, что вы перечислили в статье, *напрямую* выводится только «все есть ф-ция» aka «функции высшего порядка». Остальное, это не свойства, модели Черча как таковой… это либо, как например «ленивость» — просто опция, которая непосредственно к ФП вообще никакого отношения не имеет, либо, как например «чистота» — следствие декларативности модели. И, лично с моей т.з., все это важно понимать. Я уже говорил, что мне сложно представить, как можно «разбираться с ФП», сначала не разобравшись с декларативностью.

И именно эти первопричинные основы достойны того, чтобы их изучать в рамках курсов функционального программирования.
Я тут уже запутался в ваших тезисах. Но если вы к «первопричинным основам» относите «ленивость», то, лично для меня, это, мягко говоря, странно. Мне сложно представить, что вы на полном серьезе считаете, что, тот же Haskell функциональный, потому что он «ленивый» и там есть «монады» :-)
Её отличие как раз в том, что от порядка выбора редексов результат не зависит.

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

НЛО прилетело и опубликовало эту надпись здесь

Да. Но просто более точно здесь сказать не то, что от порядка результат не зависит, а то, что мы можем вставить любое конечное число редукций в любом порядке и потом в нормальном порядке довести терм до нормальной формы (если он доводится). Т. Е здесь важны скорее особые свойства нормального порядка.

Как пример можно кстати на скалу посмотреть. Она и императивная, и функциональная, и ленивая, и неленивая. Например:


Строгий императивный:


def print_name(name: String, shouldPrintName: Bool) { 
  if (shouldPrintName) {
    println("Your name is: " + name)   
  }
} 

Ленивый императивный:


def print_name(name: => String, shouldPrintName: => Bool) { 
  if (shouldPrintName) {
    println("Your name is: " + name)   
  }
} 

Строгий функциональный:


def print_name(name: String, shouldPrintName: Bool) : IO[Unit] { 
  if (shouldPrintName) {
    cats.effect.IO.println("Your name is: " + name)   
  } else {
    IO.pure(())
  }
} 

Ленивый функциональный:


def print_name(name: => String, shouldPrintName: => Bool) : IO[Unit] { 
  if (shouldPrintName) {
    cats.effect.IO.println("Your name is: " + name)   
  } else {
    IO.pure(())
  }
} 
Спасибо за вашу статью.

Любителям функционального стиля и python – предлагаю посмотреть на замечательную библиотеку returns, где реализованы основные монады (+типизация) и разные полезные вспомогательные штуки для работы с ними. Очень помогает делать простые вещи – просто.
Благодаря этой статье я добавил в свой план на этот год изучение функционального языка. Теперь я наконец то понял, зачем вообще. Спасибо!
И зачем? Правда интересно.
Попробовать заставить мыслить свой мозг другими категорями. Для меня это близко тому, как я после Java изучал SQL. Когда надо просто по другому думать, и это не всегда просто.

Для меня изучение какого нибудь функционального языка будет скорее развлечением, чем необходимостью для работы. Однако я надеюсь, что через годик возможность взглянуть на проблему под другим углом сможет мне помочь.
Да, если «будет скорее развлечением, чем необходимостью», то почему-бы нет. При наличии свободного времени.
Здесь особенно рьяный императивно настроенный читатель

PureScript похож на хаскель, почти близнец по синтаксису, только жадный. Это FP или еще нет? А Continuation Passing Style, где все есть функции, но они не возвращают результата, это FP или что-то другое? А call/cc в схеме, который доводит эту идею до абсурдидеала, позволяя прыгать из одного места программы в другое не хуже пресловутого goto?

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

Публикации

Истории