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

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

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

    Насколько это соответствует истине?

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

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

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

    Итак, к сути


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

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

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

    Лень — двигатель прогресса


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

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

    fibonacci2 a b = a : (fibonacci2 b (a+b))
    fibonacci = fibonacci2 1 1
    
    nfibonacci n = take n fibonacci

    Пояснение для незнакомых с хаскеллем
    fibonacci2 для двух аргументов рекурсивно строит список, первым элементом которого будет первый аргумент функции, а оставшаяся часть списка является результатом рекурсивного применения fibonacci2 ко второму аргументу b и значению (a+b). Эквивалентный (по форме!) код для питона выглядит так:
    def fibonacci2(a, b) :
        return [a] + fibonacci2(b, a+b)
    
    def fibonacci() :
        return fibonacci2(1, 1)
    
    def nfibonacci(n) :
        res = []
        data = fibonacci()
        for i in range(n) :
          res.append( data[i] )
        return res
    

    Не советую вызывать nfibonacci.

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

    Здесь особенно рьяный императивно настроенный читатель воскликнет: "Но постойте, только откровенный идиот станет реализовывать построение списка чисел Фибоначчи таким образом! Есть же очевидные решения, не приводящие к зацикливанию". И он, конечно, будет прав. Тупой перенос решения, предполагающего выполнение в рамках модели «ленивых» вычислений, в программу для «жадных» вычислений действительно не является показателем большого ума. Если предложить данную задачку программисту, который всю свою профессиональную жизнь хранил верность, скажем, языку C, то он скорее всего предложит вариант с одним циклом со счетчиком и двумя переменными состояния.

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

    def fibonacci() :
        a = 1
        b = 1
        yield a
        yield b
        while True :
          c = a + b
          yield c
          a = b
          b = c
         
    def nfibonacci(n) :
        return [e for e in itertools.islice(fibonacci(), n)]
    

    Здесь fibonacci() — генератор, который создает последовательность элемент за элементом. И в данном случае вместо fibonacci может стоять функция-генератор любой сложности. Если привести код полностью, включая подкапотный код генератора, то получим весьма сложную и полностью императивную программную конструкцию. Но окончательный вариант вполне себе «функциональный». В C++ можно было бы провернуть аналогичный трюк, заведя специальный класс Fibonachi и итераторы для него. Решение будет меняться в зависимости от особенностей языка программирования и предпочтений программиста, но цель останется общая — разделить на уровне организации программы способ построения последовательности заранее неизвестной длины и способ обработки ее элементов.

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

    Чистота — залог здоровья


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

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

    Для нас естественно ожидать, что если мы сначала определили функцию

    def f(x,y) :
      ...

    а после нее
    def g(x, y) :
      return f(y, x)
    

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

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

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

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

    Нужны ли монады в императивном мире? У меня нет устоявшегося мнения по данному вопросу. Автор этого поста уверен, что нужны. Я склонен усомниться в данном утверждении, поскольку польза использования понятия монады в функциональных программах обычна связано с тем, что некоторый алгоритм можно сформулировать безотносительно того, какие конкретно побочные эффекты данная монада скрывает. Иными словами, если пользовательский (гипотетический, еще не созданный человечеством) тип данных удовлетворяет требованиям, которые предъявляются к монаде, то записанный алгоритм для него отработает корректно. Это удобно прежде всего в теоретических изысканиях. Но есть пара нюанса. Во первых, не слишком понятно, зачем прятать в обертку побочные эффектны в языках, для которых они являются естественным явлением. Во вторых, при написании конкретных программ с конкретными типами данных и конкретной целевой архитектурой такой обобщенный алгоритм чаще всего вынужденно подвергнется реструктуризации с целью повышения производительности. Написание обобщенных алгоритмов с использованием монад в императивном стиле возможно, но целесообразность такого подхода вызывает у меня сомнения. То, что некий аналог Maybe типа std::optional из C++ объявят монадой, вряд ли как-то повлияет на практику его использования.

    Кто наблюдает за наблюдателями?


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

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

    В качестве функции высшего порядка обычно приводят map или fold. Но мы рассмотрим более тривиальную — любую — функцию двух аргументов f(x, y). В рамках модели «ленивых» вычислений аргументы данной функции будут вычислены только тогда, когда действительно понадобятся. Допустим, что первым понадобился аргумент x.

    Вычислим этот аргумент, предоставим его значение f и, кроме того, вычислим все, что можем вычислить без использования значения аргумента y. Тогда остаток вычислений вполне можно представить как новую функцию, уже от x не зависящую — например, g(y). Но в таком случае ничто не мешает нам формально представить f не как функцию двух аргументов, а как функцию одного аргумента f(x), результатом которой будет другая функция g(y). Иными словами, в рамках функционального подхода любая функция N>1 аргументов является функцией высшего порядка, поскольку может быть интерпретирована как функция одного аргумента, результатом которой является функция N-1 аргументов.

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

    def partial(f, x) :
    	def g(*args) :
    		return f(x, *args)
    	return g
    

    Вызвав функцию partial, первым аргументом которой является функция N аргументов, а вторым — значение первого ее аргумента, мы получаем функцию N-1 аргумента. Теперь новую функцию мы можем использовать везде, где можно использовать функцию N-1 аргументов. То есть получили то же самое, что и в функциональной программе. Так? Нет, не так. Если бы мы имели дело с действительно функциональной программой, то при вызове partial произошло бы вычисление части того, для чего нужно значение первого аргумента. В некоторых случаях вообще могло бы получиться, что g — константное значение. Что мы имеем в императивном аналоге? Переданное значение аргумента x просто запоминается (добавляется в контекст функции g). Когда мы вызовем g, значение x будет вынуто из закромов и просто подставлено в f. То есть разницы в форме нет, а вот в содержании — существенная.

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

    Так кто кого обманывал?


    На этом этапе изложения вполне можно поставить точку с запятой и вернутся к основному вопросу. Поскольку теперь можно сформулировать следующие утверждения:

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

    Функциональное программирование — это то, о чем вам (наверно) рассказывали. Это бета-редукция, комбинаторы неподвижной точки, монады, типизация Хиндли-Милнера и многое другое. Не надо путать обертку с содержанием. ФП базируется на не самой простой математике, не может быть освоено в течение пары вечерков за рюмкой чая; оно вряд ли сможет быть непосредственно спроецировано на ваши насущные проблемы и проекты, гарантированный и быстрый профит от этих знаний вы не получите. Но многие элементы того, что есть в функциональном подходе, заимствуются, перерабатываются и в конечном счете реализуются в языках программирования, ориентированных на разработку больших проектов. Да, они устроены иначе, чем их функциональные предки, но это не делает их менее полезными. Только клинический идиот будет на серьезных щах вещать о том, что хаскель — плохой язык, потому что на нем сложно написать программу для какого-нибудь бухучета. Человек, обремененный наличием интеллекта, даже с позиций своей профессиональной деятельности, без глубокого погружения в хитросплетения теории вполне способен понять, какие именно практики из функционального программирования стоит перенять для того, чтобы сделать свой код лучше. За убедительную демонстрацию чего выражаю благодарность PsyHaSTe.

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

    Подробнее
    Реклама

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

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

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

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

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


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

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

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


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

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

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

                    0
                    Автор выполнил свою работу блестяще. Только последний абзац следовало бы сделать первым, чтобы обезопасить людей с подорванной подобными глупостями психикой от приступа душевных терзаний.
        +1
        Оставлю тут ленивую версию кода на 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)
        



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

          0

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


          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 функции.

            +3
            Если вдруг вопросы, почему редьюс лучше: если убрать строчку с проверкой, то исчезнет переменная 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

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

              0

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


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

                0

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

                  +1

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


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


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


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

                    +1

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


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

                      –1

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

                        +1

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

                          0

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


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

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

                            +2

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


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


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

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

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


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

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




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


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

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

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

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

                                  0

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

                                    +1

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


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

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

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


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

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

                                        0

                                        Мне один знакомый питонист тоже пытался mypy продать, я её немного поковырял — в общем, оно разваливается, похоже, от малейшего чиха (то есть, от малейшей попытки делать что-то более интересное, чем позволяет система типов языков вроде С), да и добрая часть библиотек неаннотирована.

                                          0

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

                                            0

                                            Уф, это несколько месяцев назад было, правильно б вспомнить. Что я точно помню — я там пытался сделать что-то вроде Either, но оно вело себя как untagged union, и пришлось руками делать диспатчинг по типу (и Either A A не работало). И newtype там тоже как-то не особо было, и написать что-то вроде type family не получилось.


                                            Попробую поиграться и воспроизвести.

                                          +1
                                          Ну, одна из причин почему я предпочитаю статически типизированные япы

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

                                            0

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


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

                                              +2

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

                                                0

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

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


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


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


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


                                                    0
                                                    Или поставить возвращаемый тип в зависимость от входных данных?

                                                    Уважаю языки, которые позволяют так делать, ЕВПОЧЯ.

                                                      0

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

                                                        +1

                                                        Тип (b : Bool) -> if b then Int else String вполне себе не нужно угадывать.

                +2

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

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

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

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

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

                  Такие дела.
                    0

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

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

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

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

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

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


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

                          0

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

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

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

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

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

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

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


                                Касаемо питона, пункт 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

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

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

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

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

                            А программы на идрисе — императивные? А то он строгий по умолчанию.


                            А программы на функциональных языках, для теорий типов которых выполняется strong normalization theorem, и потому для которых порядок вычислений вообще не важен (Coq, доказуемо завершимое подмножество агды и идриса)?

                              –1
                              А программы на идрисе — императивные? А то он строгий по умолчанию.

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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


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

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

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


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


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


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


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


                                      +3

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

                                        +1

                                        Именно так.

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

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


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

                                  –1

                                  Знакомо ли вам понятие «абстракция»?

                                  +3

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


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

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


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


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

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


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

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

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

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

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


                                      img


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


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

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

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


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


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

                                            +1

                                            Тут нет никаких циклов, тут просто последовательное выполнение. На обычном дотнете это выглядело бы как огромный трай кетч с упаковыванием значения в 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 = ...

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

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

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


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

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


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

                                                  0

                                                  ну делаете


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

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

                                                    0

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


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

                                                      0

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


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

                                                        0

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

                                                          0

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

                                                  –1
                                                  Вообще то в шарпе уже есть подобие монад. Только вместо 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 ...
                                                  
                                                    0

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


                                                    _ = 5;
                                                    _ = 10;

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

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

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

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

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


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


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

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

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


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

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

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

                                                    +1

                                                    Лень с линейными типами вообще отлично сочетается, чтобы ее убрать, когда у аргумента multiplicity — 1 :)


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

                                                      0

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

                                                        0

                                                        Я довольно неаккуратно имел в виду «класс функций, для которых существует достаточно общий алгоритм доказательства завершимости». Понятно, что вы можете взять любой имеющийся чекер и добавить туда вот прям синтаксическую проверку на (λx.0)((λx.x x) (λx.x x)) (который завершается при ленивой стратегии, но расходится при строгой), но это несерьёзно.

                                                        –1

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

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

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


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

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

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

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

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


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

                                                            +1
                                                            Даже если таких людей пара человек — это стоящая работа
                                                    0

                                                    del

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

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


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

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

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

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

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

                                                                      0

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


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

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

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

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

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


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


                                                                    Такие дела)

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

                                                                                                      Какой закон для монады помешает сначала записать 2.txt, а после прочитать 1.txt? Без шуток, может я действительно что-то забыл или не знал.
                                                                                                        +1
                                                                                                        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-


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

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

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

                                                                                                              –1

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

                                                                                                              +2

                                                                                                              IO — это такой особый State, если хотите, а для State там есть зависимость через, собственно, состояние.


                                                                                                              Ровно поэтому state token (RealWord# всякий) внутри IO и передаётся туда-сюда — чтобы в следующем действии его получить, вам надо выполнить предыдущее (производящее это действие).

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

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


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

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

                                                                                                                      Мне лень лезть в haskell report, чтобы выяснить, какие требования к IO там формально предъявляются (у меня для таких развлечений уже C++ есть с его стандартом), но я почти уверен, что таковая реализация IO этому репорту соответствовать не будет.


                                                                                                                      И да, я согласен с предыдущим оратором — если я пишу putStr "Hello " >> putStrLn "world", то я ожидаю строку Hello world, а не worldHello.

                                                                                                                  –1
                                                                                                                  Лучше сам отвечу )
                                                                                                                  Мешает, конечно, принятое правило редукции. У меня что-то из головы вылетело, что в конкретной реализации аргументы получают значения все-таки не в произвольном порядке.
                                                                                                                    0

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

                                                                                                                      –1

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

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

                                                                                                    Нет. Возьмём инстанс Monad для стрелки...


                                                                                                    Prelude> :m + Debug.Trace
                                                                                                    Prelude Debug.Trace> foo a = "foo" `traceShow` a * 2
                                                                                                    Prelude Debug.Trace> bar a = "bar" `traceShow` a * 3
                                                                                                    Prelude Debug.Trace> (foo >> bar) 1
                                                                                                    "bar"
                                                                                                    3

                                                                                                    Правда, его тогда в main загнанть не получится, но это другой вопрос.

                                                                                                      0

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


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

                                                                                                        –1
                                                                                                        Почему? Может ради него всё и писалось.
                                                                                                          0

                                                                                                          Сорри, я с утра не очень правильно распарсил контекст тогда.


                                                                                                          Но так как тут ссылались на описание >> для всего класса Monad и вроде как упоминали об операции >> в общем смысле, то, ИМХО, важно подчеркнуть, что обсуждаемое поведение >> — следствие того, как написано IOState, и ST, и подобные монады), а не следствие самого определения монады.

                                                                              0
                                                                              Изолировать проблему в отдельной хорошо видимой части программы не означает спрятать ее под коврик. Ну никаким боком. Вы просто будете знать (в том числе на этапе компиляции), где у вас чистые функции, а где есть IO. И нет, это в общем-то совсем не много усилий. В других подходах они тоже будут, просто выражены иначе.
                                                                                0
                                                                                Я немного про другое. Нужно понимать что где-то всегда будут грязные функции и от них не избавиться.
                                                                                  +1
                                                                                  Я именно об этом. IO позволяет их изолировать, и точно знать, где они. Вообще чистота — это давно уже не про то что нечистых функций нет, а про то, что мы точно знаем, где они.
                                                                                    –2
                                                                                    К сожалению, история гораздо более сложная, потому что вызов функции — это тоже эффект. И seq — это эффект. И unsafeRunIO — тоже эффект. И появились они не из вредности, а потому что нужно на практике. Это я к тому, что не следует идеализировать. Haskell форсирует, конечно, дисциплину, но точности это не добавляет, потому что в библиотеках может быть всякое.
                                                                                      0
                                                                                      К сожалению, история гораздо более сложная, потому что вызов функции — это тоже эффект.

                                                                                      В каком смысле? Это всего лишь подстановка definiens вместо definiend. Вы же шаг бета-редукции не считаете эффектом?


                                                                                      И seq — это эффект.

                                                                                      Только если у вас есть ⊥. Но хаскелисты прикидываются, что у них его нет.


                                                                                      И unsafeRunIO — тоже эффект. И появились они не из вредности, а потому что нужно на практике.

                                                                                      Без unsafePerformIO на практике можно обойтись вообще всегда. Оно всего лишь позволяет срезать углы (и лично я позволяю себе срезать углы таким образом либо в тестах, либо в compile-time-коде, где все проблемы вылезут при компиляции).

                                                                                        0
                                                                                        Только если у вас есть ⊥. Но хаскелисты прикидываются, что у них его нет.

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

                                                                                          0

                                                                                          А иначе seq не влияет вообще ни на что. Если у вас нет ⊥, то seq a b definitionally equivalent'ен b всегда, для любого a, как flip const a b.


                                                                                          То, что seq как-то влияет на производительность, следствие того, что seq надо привести a в WHNF, чтобы понять, ⊥ это или нет.

                                                                                            0

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

                                                                                              0

                                                                                              С одной стороны, да, безусловно, компиляторы всё ещё недостаточно умны, чтобы strictness analyzer там всё правильно как надо разрулил.


                                                                                              С другой — ИМХО инструменты от bang pattern'ов (пусть даже они определяются через дешугаринг в seq) до тех же линейных типов с аннотациями типа «аргумент используется не менее одного раза», «аргумент используется ровно один раз» как-то более приятны, и рассуждать о них проще, чем о seq. seq лично для моей ментальной модели как-то не очень совместим с local reasoning. Да и даже bang pattern'ов лично мне почти всегда достаточно.

                                                                                            0

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

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

                                                                                            arxiv.org/pdf/1807.05923.pdf
                                                                                              0
                                                                                              Зависит от того, чем мы считаем beta-редукцию. Если она реализована, как операция, через изменение окружения и eval, а в языках программирования так и делается, то это эффект.

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


                                                                                              Вот здесь вычитал arxiv.org/pdf/1807.05923.pdf

                                                                                              Пропустил эту статью, спасибо! Но вот прямо сейчас внимательно читать в контексте дискуссии я её не готов, там есть более конкретная ссылка?

                                                                                0

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


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

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

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

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

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

                                                                                          +1

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

                                                                                            0

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

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

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

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

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

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

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


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


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

                                                                                    0

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


                                                                                    Другое дело, что изучать ФП без уклона в ТТ лучше действительно по хаскелю. Просто куда ни плюнь, в современном хаскеле то DataKinds, то TypeFamilies, то ещё что подобное. Полезно понимать, откуда и куда эти вещи растут.