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

Когда я учился программированию меня учили что рекурсия это плохо и нельзя надеятся что компилятор сможет заменить написанную тобой рекурсию на итеративный процесс. В какой то степени нас учили искать способ замены рекурсии итеративным алгоритмом(в те годы терминология была не очень проработана — понятия размыты). На сколько я понимаю замена рекурсии итеративным алгоритмом это одна из базовых задач программирования. Здорово когда она уже решена в компиляторе (в интерпретаторе) и в языке таком как диалект LISP-а который используется для примеров в книге известной под абревиатурой SICP, но в любом случае не помешает знание о том как эта задача решается. Как минимум это поможет вам лучше понимать работу компилятора и эффективнее его использовать.


Книга Structure and Interpretation of Computer Programs известна даже через аббревиатуру своего названия SICP. И хотя я ее раньше и не читал, прочитав ее я понял что меня когда-то учили на материалах из этой книги.

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

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

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

  1. Метод Герона

  2. Метод Бахшали

  3. Посчитать цифру за цифрой

  4. Экспоненциальное тождество

  5. Итерационный метод с двумя переменными

  6. Итерационные методы получения обратных квадратных корней

  7. Ряд Тейлора

  8. Продолжение дробь

  9. Приближения, зависящие от представления с плавающей запятой

  10. Отрицательный или комплексный квадрат

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

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

Но давайте все таки:

Вернемся к заявленной теме про хвостовую рекурсию.

Понятие о хвостовой рекурсии вводится в книге SICP со следующим примером на языке Scheme (диалект языка LISP) для реализации функции вычисления факториала числа

n! = n · (n − 1) · (n − 2) · · · 3 · 2 · 1.

Первый вариант реализации такой функции выглядит предельно компактно:

    (define (factorial n)
        (if (= n 1)
            1
            (* n (factorial (- n 1)))))

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

(define (factorial n)
    (fact-iter 1 1 n))
(define (fact-iter product counter max-count)
    (if (> counter max-count)
        product
        (fact-iter (* counter product)
            (+ counter 1)
            max-count)))

Хотя это не конечный вариант, конечный вариант получается после применения некоторой оптимизации синтаксиса:

(define (factorial n)
    (define (iter product counter)
        (if (> counter n)
            product
            (iter (* counter product)
                (+ counter 1))))
    (iter 1 1))

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

линейный рекурсивный процесс вычисления 6! (первая реализация алгоритма)
линейный рекурсивный процесс вычисления 6! (первая реализация алгоритма)

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

процесс вычисления 6! (2-я и 3-я реализация алгоритма)
процесс вычисления 6! (2-я и 3-я реализация алгоритма)

Если мы сравним эту реализацию на языке LISP с реализацией на языке Си и с циклом, что мы увидим:

int factorial(int n)
{
    int res = 1;
    int counter = 1;
    while ( n > counter )
    {
        res *= counter;
        counter += 1;
    }
    return res;
}

Так что же мы обнаружим? В Си-коде у нас пропал IF ! Дело в том что оператор цикла while это и есть на самом деле самый обычный оператор IF в сочетании с оператором безусловного перехода GOTO который всегда возвращает поток исполнения к этому самому IF переобозначенному как while.

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

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

Интересно что авторы этой книги объявляют начичие инструкций циклов (special-purpose “looping constructs” such as do, repeat, until, for, and while) фактически недостатком языка, так как

that most implementations of common languages (including Ada, Pascal, and C) are designed in such a way that the interpretation of any recursive procedure consumes an amount of memory that grows with the number of procedure calls, even when the process described is, in principle, iterative. As a consequence, these languages can describe iterative processes only by resorting to special-purpose “looping constructs” such as do, repeat, until, for, and while.

Если перевести описание причины из цитаты, то она звучит примерно так:

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

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

It will execute an iterative process in constant space, even if the iterative process is described by a recursive procedure. An implementation with this property is called tail-recursive.

При этом в сноске мелкими буквами вы можете прочитать что

Tail recursion has long been known as a compiler optimization trick. A coherent semantic basis for tail recursion was provided by Carl Hewi (1977)

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

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

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

В любом случае я призываю всех внимательно прочитать эту книгу Structure and Interpretation of Computer Programs до конца и прорешать примеры приведенные в этой книге не только на языке который используется в книге, но и на языке на котором вы работаете или собираетесь работать. Это очень хорошая книга и я не знаю книги в которой лучше определены и изложены базовые термины програмирования как способа описания реальных вычислительных задач (кодирования). Я сам когда-то учился на материалах из этой книги, но я не считаю способность переписать алгоритм с рекурсивными вызовами, алгоритмом на базе всем понятных циклов какой-то сверх способностью, доступной только разработчикам компиляторов.

Комментарии (68)


  1. Timick
    12.02.2025 04:58

    Не вижу проблем с рекурсией если количество вызовов строго ограниченно. В противном случае можно оказаться в call stack overflow. Ну это будет сильно дурацкая имплементации как в задаче заливки фигуры.


    1. vadimr
      12.02.2025 04:58

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


  1. vadimr
    12.02.2025 04:58

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


    1. rukhi7 Автор
      12.02.2025 04:58

      Что касается человеческой привычки, то она формируется первым изученным языком программирования.

      ну не скажите! первым я изучал Basic (если не рассматривать коды на калькуляторе как язык :) ), практически парралельно Фортран и PL/1, еще Фокал, но родным(так сказать) я для себя ощущаю любой Си-подобный язык от чистого Си через С++, до C# и даже Java-код для меня прост и понятен с первого взгляда. Конечно более менее навороченные конструкции всегда требуют задуматься, но это уже не зависит от языка.

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


      1. vadimr
        12.02.2025 04:58

        Если в привычных вам языках рекурсия является производной конструкцией, то вы её и воспринимаете как таковую.

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


        1. rukhi7 Автор
          12.02.2025 04:58

          Если же вы начинали со старого Бейсика и калькуляторных кодов

          На сколько я понимаю Scheme постарше Бейсика будет. Вообще я начинал с алгебры Буля и схем на логических элементах в стиле VHDL, а еще раньше с таблицы умножения (впрочем как и вы наверно) если на то пошло.


          1. vadimr
            12.02.2025 04:58

            Я имел в виду Бейсик с номерами строк, где рекурсия невозможна в принципе.


            1. rukhi7 Автор
              12.02.2025 04:58

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


              1. vadimr
                12.02.2025 04:58

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

                Что касается того, как вас учили, то это частное мнение вашего преподавателя. Лично я его не одобряю, и нас учили по-другому.


    1. rukhi7 Автор
      12.02.2025 04:58

      а теория вычислений идёт от рекурсии

      я бы посмотрел как бы вы реализовали, например, Быстрое Преобразование Фурье (FFT) через рекурсию. А это такой хороший пример из теории построения таких практических сложных вычислительных алгоритмов. Теории бывают разные.


      1. vadimr
        12.02.2025 04:58

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

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

        А с циклом надо углубляться в его семантику.

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

        *) Встанет, правда, вопрос с аппликативным порядком вычислений, но это второе дело.


        1. rukhi7 Автор
          12.02.2025 04:58

          Я говорю о вычислениях в смысле evaluation, а не calculation.

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

          Конкретно на Scheme БПФ будет сложнее написать только из-за отсутствия матриц в ядре языка.

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


          1. vadimr
            12.02.2025 04:58

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

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

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


            1. rukhi7 Автор
              12.02.2025 04:58

              Если вы хотите получить наиболее быструю реализацию БПФ, то вам надо писать в машинных кодах

              а вы разве не знаете что

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

              умеют и оптимизацию для:

              глубокие повторяющиеся вычисления будут выполняться векторными машинными командами

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

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


              1. vadimr
                12.02.2025 04:58

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


                1. adeshere
                  12.02.2025 04:58

                  > Но это и к циклам относится в той же самой мере.

                  Вот как раз на циклы все современные компиляторы натасканы круче некуда. Если эффективность не стоит на последнем месте, конечно. В том же фортране никому и в страшном сне не придет в голову заменять массивный оператор a=f(b), или a=b+c, где a, b и c- многомерные массивы, а f -

                  элементная функция

                  т.е. каждый элемент массива a = результат применения функции f к соответствующему элементу массива и b

                  на рекурсию. Мало того, что далеко не всякий фортранист с этим справится, так еще и машинный код с 99.9999%-ной гарантией получится в разы менее эффективным.


                  1. vadimr
                    12.02.2025 04:58

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

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

                    А так всё зависит от конкретного языка и его реализации. В языке Scheme цикл является макроопределением над рекурсией.


                    1. adeshere
                      12.02.2025 04:58

                      > Пишете здравицу за циклы, а приводите в пример массивный оператор.

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

                      Я не сомневаюсь, что рекурсией его записать тоже можно. Вот только удобно ли? А главное, зачем? Напишите, для примера, рекурсию для двух-трехмерного массива - и сравните, какой код

                      легче понять среднестатистическому программеру/математику?

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

                      Я уж не говорю, что если вдруг компилятор окажется недостаточно умным, и вдруг все-таки реализует рекурсию через call, то на одной только передаче параметров (которая в фортране реализована максимально эффективно!) мы сразу проиграем не проценты, а много крат.

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

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

                      среднестатистическом программисте?

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

                      А в остальном Вы, конечно же, правы.


                      1. vadimr
                        12.02.2025 04:58

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

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

                        Вот три программы, о которых Вы говорили:

                        program m1
                        
                          integer, parameter :: s = 512
                          real (8), allocatable, dimension (:,:,:) :: a, b, c
                        
                          allocate (b(s,s,s), c(s,s,s))
                          call random_number (b)
                          call random_number (c)
                          a = b+c
                          print *, (sum(a))
                        
                        end program m1
                        program m2
                        
                          integer, parameter :: s = 512
                          real (8), allocatable, dimension (:,:,:) :: a, b, c
                          integer :: i,j,k
                        
                          allocate (b(s,s,s), c(s,s,s))
                          call random_number (b)
                          call random_number (c)
                        
                          allocate (a(s,s,s))
                          do k=1,s
                            do j=1,s
                              do i=1,s
                                a(i,j,k) = b(i,j,k)+c(i,j,k)
                              end do
                            end do
                          end do
                        
                          print *, (sum(a))
                        
                        end program m2
                        program m3
                        
                          integer, parameter :: s = 512
                          real (8), allocatable, dimension (:,:,:) :: a, b, c
                          integer :: i,j,k
                        
                          allocate (b(s,s,s), c(s,s,s))
                          call random_number (b)
                          call random_number (c)
                        
                          allocate (a(s,s,s))
                        
                          call recsum(a, b, c, s, s, s)
                        
                          print *, (sum(a))
                        
                        contains
                        
                         pure recursive subroutine recsum (a, b, c, i, j, k)
                        
                            real (8), dimension (s,s,s) :: a, b, c
                            integer :: i, j, k
                            intent (in) :: b, c, i, j, k
                            intent (out) :: a
                        
                            if (i==0) then
                              return
                            else
                              if (j==0) then
                                call recsum (a, b, c, i-1, s, s)
                              else
                                if (k==0) then
                                  call recsum (a, b, c, i, j-1, s)
                                else
                                  a(k,j,i) = b(k,j,i)+c(k,j,i)
                                  call recsum (a, b, c, i, j, k-1)
                                end if
                              end if
                        
                          end subroutine recsum
                        
                        end program m3


                      1. adeshere
                        12.02.2025 04:58

                        > Вот три программы, о которых Вы говорили:

                        Спасибо, действительно интересно!

                        Мне конечно сложно оценивать, так как я и мои друзья правда "...привыкли представлять себе массивный оператор таким образом". Но на мой субъективный вкус, тройной цикл в программе m2 все-таки попонятнее, чем нанизанные if - else в m3. Поэтому было бы очень интересно узнать: а как оценивается этот код с точки зрения человека, привыкшего к рекурсивной форме выражения мыслей?

                        > Но проблема в том, что gfortran не понимает, что здесь рекурсия хвостовая.

                        Давайте все-таки разделим два эти вопроса? Или даже точнее три:

                        1) Читаемость и понятность кода с точки зрения типичного программиста
                        2) Способность конкретного компилятора эффективно оптимизировать рекурсивные вызовы
                        3) Насколько пострадает задача (1), если мы будем вынуждены искусственно помогать компилятору

                        справиться с (2)?

                        допустим, он такое умеет

                        Мое субъективное мнение:
                        1) Циклы все же понятнее (Но! это зависит от "воспитания" и предыдущего опыта);
                        2) Компилятору наверное проще оптимизировать цикл, чем рекурсию?
                        3) Если такая задача становится актуальной, значит выбран не самый подходящий для данного случая язык программирования

                        Ну и конечно, наилучшим решением задач (1) и (3) я по-прежнему считаю массивные операторы. А задачу (2) компилятору в этом случае вообще не нужно решать ;-).


                      1. vadimr
                        12.02.2025 04:58

                        С моей точки зрения, если вам надо сложить два трёхмерных массива, то надо брать Фортран и массивный оператор a=b+c и не выпендриваться, так как это инструмент, специально предназначенный для решения этой задачи. А писать рекурсивные подпрограммы на Фортране – извращение, даже притом, что я люблю и ценю рекурсию как способ выражения своих мыслей. В каждом языке существует определённая идиоматика и культура написания кода.

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

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

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


                      1. adeshere
                        12.02.2025 04:58

                        > Например, массивный оператор не обязан внутри себя выполняться последовательно

                        Кстати, да! Что резко расширяет свободу компилятора оптимизизировать или распараллеливаать вычисления,

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

                        Но тогда получается, что оптимизация рекурсивного алгоритма - это на порядок более сложная задача для компилятора, чем даже оптимизация цикла?!

                        > А для компилятора в принципе что цикл, что рекурсия – один фиг.

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

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

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


                      1. vadimr
                        12.02.2025 04:58

                        Но тогда получается, что оптимизация рекурсивного алгоритма - это на порядок более сложная задача для компилятора, чем даже оптимизация цикла?!

                        Почему?


                      1. adeshere
                        12.02.2025 04:58

                        Но тогда получается, что оптимизация рекурсивного алгоритма - это на порядок более сложная задача для компилятора, чем даже оптимизация цикла?!

                        > Почему?

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

                        В общем, определенные причины так думать у меня есть, но не более того. Для получения обоснованного ответа надо вызывать чистильщика обращаться к специалисту по компиляторам. Такому, как уже упомянутый здесь уважаемый @xortator. Ну или может еще кто-то откликнется?


                      1. IUIUIUIUIUIUIUI
                        12.02.2025 04:58

                        Компилятору наверное проще оптимизировать цикл, чем рекурсию?

                        Компилятору проще оптимизировать то, смысл чего он «понимает». При этом даже loop-invariant code motion можно делать не всегда, потому что не факт, что цикл выполнится хотя бы один раз, и не факт, что у LICMнутого выражения нет сайд-эффектов.

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

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


                      1. vadimr
                        12.02.2025 04:58

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

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


                      1. vadimr
                        12.02.2025 04:58

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


                      1. vadimr
                        12.02.2025 04:58

                        Если убрать множественные рекурсивные вызовы, дурящие голову компилятору Фортрана, и оставить одномерный массив такого же размера:

                        program m0
                        
                          integer, parameter :: s = 512*512*512
                          real (8), allocatable, dimension (:) :: a, b, c
                        
                          allocate (b(s), c(s))
                          call random_number (b)
                          call random_number (c)
                        
                          allocate (a(s))
                        
                          call recsum(a, b, c, s)
                        
                          print *, (sum(a))
                        
                        contains
                        
                          pure recursive subroutine recsum (a, b, c, i)
                        
                            real (8), dimension (s) :: a, b, c
                            integer :: i
                            intent (in) :: b, c, i
                            intent (inout) :: a
                        
                            if (i==0) then
                              return
                            else
                              a(i) = b(i)+c(i)
                              call recsum (a, b, c, i-1)
                            end if
                        
                          end subroutine recsum
                        
                        end program m0

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


                      1. adeshere
                        12.02.2025 04:58

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

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

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


                      1. AlexAiZzz
                        12.02.2025 04:58

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

                        // Type your code here, or load an example.
                        int factorial(int n) {
                            if (n == 1) 
                                return 1;
                            else
                                return n * factorial(n - 1);
                        }
                        
                        int main() {
                            return factorial(5);
                        }
                        factorial:
                                mov     eax, 1
                                cmp     edi, 1
                                je      .L1
                        .L2:
                                mov     edx, edi
                                sub     edi, 1
                                imul    eax, edx
                                cmp     edi, 1
                                jne     .L2
                        .L1:
                                ret
                        main:
                                mov     eax, 120
                                ret

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


                      1. adeshere
                        12.02.2025 04:58

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

                        $-)

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

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

                        Дисклеймер

                        Главное - это не забывать, что в действительности все не так, как на самом деле (с). На заре моей компьютерной молодости это было моим девизом ;-)

                        UPD

                        Я этой фразой много лет пользовался, не зная, кто автор (дело было еще до интернета). А недавно открыл, что это Станислав Ежи Лец. Хорошо топтать плечи гигантов! ;-)


                  1. IUIUIUIUIUIUIUI
                    12.02.2025 04:58

                    Я в хаскеле спокойно пишу условный mapM_/forM_, который, вообще говоря, реализован рекурсией-свёрткой, да ещё и абстрактно поверх любого тайпкласса Foldable. И ничего, получаю производительность на уровне C++. Функциональные компиляторы натасканы на рекурсию так же, как императивные — на циклы.


                    1. rukhi7 Автор
                      12.02.2025 04:58

                      А вы попробуйте Быстрое Преобразование Фурье в хаскеле написать с производительностью на уровне С++, вот это будет номер!

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


                      1. IUIUIUIUIUIUIUI
                        12.02.2025 04:58

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

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


                  1. AlexAiZzz
                    12.02.2025 04:58

                    В том же фортране никому и в страшном сне не придет в голову заменять массивный оператор a=f(b), или a=b+c, где a, b и c- многомерные массивы, а f - на рекурсию.

                    Я кажется понял в чем проблема в дискуссии :)

                    "Человек купивший себе молоток везде видит гвозди" :)

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


      1. Dooez
        12.02.2025 04:58

        БПФ же как раз определен через рекурсию, реализовать его рекурсивно вовсе не сложно.

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


  1. vadimr
    12.02.2025 04:58

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

    В языке Scheme, использующемся в SICP, это одно и то же.

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


    1. rukhi7 Автор
      12.02.2025 04:58

      Можно обойтись примитивной рекурсивной формой letrec.

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

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


      1. vadimr
        12.02.2025 04:58

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

        В каких-то реализациях языков вход в каждый инстанс рекурсивной функции может подразумевать небесплатное создание нового контекста, но ведь и каждая итерация цикла потенциально может это делать (например, если в языке PL/I поместить внутрь цикла begin-блок). Это не вопрос рекурсии и цикла самих по себе.


        1. rukhi7 Автор
          12.02.2025 04:58

          и так понятно, что пара команд call/ret прагматически эквивалентна jmp.

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

          На самом деле call это и есть jmp только с дополнительной нагрузкой (а icall, это расширенный ijmp).


          1. vadimr
            12.02.2025 04:58

            Я говорю о том, что пару непосредственно следующих друг за другом команд call f / ret, представляющую собой реализацию в машинном языке окончания хвостово-рекурсивной функции (рекурсивный вызов и потом сразу выход), прагматически можно заменить на одну команду jmp, что и называется собственно элиминацией хвостовой рекурсии при кодогенерации. Это не совсем формально эквивалентно, потому что пара call/ret ещё оставит адрес возврата над верхушкой стека, что в машинном коде иногда используется для определения своего адреса, но в прагматическом случае обычного рекурсивного вызова так можно сделать.


            1. rukhi7 Автор
              12.02.2025 04:58

              представляющую собой реализацию в машинном языке окончания хвостово-рекурсивной функции (рекурсивный вызов и потом сразу выход)

              тут я что-то не понял! Как это: "потом сразу выход"? Потом сразу код функций по новой и новый рекурсивный вызов, выходы будут когда-то потом, если будут.

              Я на ассемблерах очень много писал, по машинным языкам меня не запутаешь.


              1. vadimr
                12.02.2025 04:58

                Ну вот вы в своей статье привели код на Scheme:

                (define (iter product counter)
                        (if (> counter n)
                            product
                            (iter (* counter product)
                                (+ counter 1))))

                Если писать на ассемблере, то это будет что-то вроде (предполагая, что counter, n и product лежат в регистрах и, соответственно, являются именами регистров):

                iter cmp counter, n
                     bgt exit
                     mul product, counter
                     inc counter
                     call iter
                exit ret

                Строки 5 и 6 (пара call/ret) – это собственно реализация хвостовой рекурсии. И их можно заменить просто одним операторомjmp:

                iter cmp counter, n
                     bgt exit
                     mul product, counter
                     inc counter
                     jmp iter
                exit ret

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


                1. rukhi7 Автор
                  12.02.2025 04:58

                  Строки 5 и 6 (пара call/ret) – это собственно реализация хвостовой рекурсии. И их можно заменить просто одним оператором jmp

                  вы видимо так увлеклись идеей переубедить меня, что, кажется не заметили что вы заменили только call на jmp! ret остался на месте :) !

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


                  1. vadimr
                    12.02.2025 04:58

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


                    1. rukhi7 Автор
                      12.02.2025 04:58

                      ну да я попал в ту же ловушку - думал о своем! Вы меня все таки запутали :) .

                      Просто надо было тогда код полностью писать:

                      с рекурсией:

                      iter: cmp counter, n
                       bgt exit
                       mul product, counter
                       inc counter
                       call iter
                       exit: ret
                      
                      factorial:  set counter, 1
                       set n, param
                       call iter
                       set acc, counter//значение надо вернуть каким то стандартным способом
                       ret

                      без рекурсии

                      factorial:  set counter, 1
                       set n, param
                       iter: cmp counter, n
                       bgt exit
                       mul product, counter
                       inc counter
                       jmp iter
                       exit:  set acc, counter
                       ret

                      все равно ret это довесок к call, действительно без call не нужен ret, только еще код итерации надо заинлайнить.

                      Все равно , с точки зрения порядка исполнения jmp заменяет только call,

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


                      1. vadimr
                        12.02.2025 04:58

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

                        Я хочу сказать только то, что коды команд call и ret, следующие в тексте программы сразу друг за другом, вместе прагматически эквивалентны одному jmp.


                      1. rukhi7 Автор
                        12.02.2025 04:58

                        Теперь я хотя бы понял про что вы толкуете. С этим можно согласиться в общем.

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


          1. adeshere
            12.02.2025 04:58

            > На самом деле call это и есть jmp только с дополнительной нагрузкой (а icall, это расширенный ijmp).

            Именно что!

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

            и заменит ли циклом

            дело даже не в том, что нет времени, но у меня компилятор 2008 года, так что ответ про современные компиляторы мы все равно не получим

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


            1. AlexAiZzz
              12.02.2025 04:58

              Тот же gcc вполне себе распознает хвостовую рекурсию уже достаточно давно, нужно лишь указать тип оптимизации, работает при -O2 и -O3 как минимум

              Пожалуйста с codebolt для факториала:

              factorial(int):
                      mov     eax, 1
                      cmp     edi, 1
                      je      .L1
              .L2:
                      mov     edx, edi
                      sub     edi, 1
                      imul    eax, edx
                      cmp     edi, 1
                      jne     .L2
              .L1:
                      ret

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


              1. AlexAiZzz
                12.02.2025 04:58

                И это переведено не верно:

                that most implementations of common languages (including Ada, Pascal, and C) are designed in such a way that the interpretation of any recursive procedure consumes an amount of memory that grows with the number of procedure calls, even when the process described is, in principle, iterative. As a consequence, these languages can describe iterative processes only by resorting to special-purpose “looping constructs” such as do, repeat, until, for, and while.

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


              1. rukhi7 Автор
                12.02.2025 04:58

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


                1. AlexAiZzz
                  12.02.2025 04:58

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


              1. IUIUIUIUIUIUIUI
                12.02.2025 04:58

                Распознавать-то распознаёт, только иногда делает с этим адовую ерунду, когда там требуются какие-то дополнительные оптимизации: https://gcc.godbolt.org/z/bh6Gx5hxq

                Мне лень по ассемблеру строить полный call graph, но у clang есть хотя бы один рекурсивный путь с call, а gcc делает что-то либо очень гениальное, либо очень тупое для -DSHITTY_CODEGEN-случая (и там тоже есть какие-то рекурсивные пути с call — gcc'шный кодген мне понимать сильно тяжелее, чем clangовский в этом случае).

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


  1. viordash
    12.02.2025 04:58

    вероятно у вас в сишном коде ошибка, должно быть while ( counter <= n)


    1. rukhi7 Автор
      12.02.2025 04:58

      спасибо, поправил! while ( n > counter) мне больше нравится.


      1. AlexAiZzz
        12.02.2025 04:58

        Ну и кстати ближе к исходному определению факториала будет:

        int factorial(int n)
        {
            int accumulator = n;
            while (--n)
            {
                accumulator *= n;
            }
            return accumulator;
        }


        1. rukhi7 Автор
          12.02.2025 04:58

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

          Кажется вопрос еще ждет своего увлеченного исследователя.


          1. AlexAiZzz
            12.02.2025 04:58

            Про совершенство я не знаю :)

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

            n! = \prod^n_{i=1} i


          1. AlexAiZzz
            12.02.2025 04:58

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

            Кажется вопрос еще ждет своего увлеченного исследователя.

            Кстати, наверное на это вопрос могу ответить :)

            Я язык не выбирал :) это сделали Вы, а я отвечал на том же.

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


            1. rukhi7 Автор
              12.02.2025 04:58

              вы еще декларативные языки забыли (видимо) упомянуть. XSLT вот например очень интересно работает, причем работает на практике!


              1. AlexAiZzz
                12.02.2025 04:58

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


        1. Timick
          12.02.2025 04:58

          Перемножение на единицу тут вижу я.


          1. AlexAiZzz
            12.02.2025 04:58

            В каком месте?


            1. Timick
              12.02.2025 04:58

              когда n==1


              1. AlexAiZzz
                12.02.2025 04:58

                А, с этой точки зрения. Ну так я не оптимизировал код, он и в определения факториала умножает на единицу. Но на самом деле в коде все равно будет переход по сравнению с нулем и чтобы и не умножать на 1, перед переходом будет сравнение с 1, что будет делаться во внутрях через вычитание. Умножение на 1, как на нейтральный элемент скорее всего будет выполняться достаточно эффективно, так что я тут без измерений не буду утверждать что будет быстрее: вычесть единицу на каждой итерации или один раз умножить на 1. Зависит оттого что реализовано в схемотехнике. Вполне вероятно зависит от n.


          1. rukhi7 Автор
            12.02.2025 04:58

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


  1. Jijiki
    12.02.2025 04:58

    рекурсии и фракталы ) пока делится идёт рекурсия ) интересно будет так работать? или будет упор в стек (просто предположил возможно без рекурсии наверно, я в этом не силён )

    https://www.reddit.com/r/GraphicsProgramming/comments/1ijw78c/feature_demo_video_of_surfacestable_fractal/