0,0
рейтинг
21 марта 2012 в 16:27

Разработка → Что не так с циклами for? перевод

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

Сегодня Эллиотт Расти Харольд (Elliotte Rusty Harold) опубликовал свои сомнения по поводу возможных изменений. Один из главных заданных им вопросов: “Почему вы ненавидите цикл for”(«Why Hate the for Loop?»)?

Я не знаю, что заставляет людей так страстно мечтать об избавлении от циклов for. Это не первый и даже не второй раз, когда теоретики от мира computer science восстают против них.

Было бы неправильным думать, что один лишь Эллиотт подвергает сомнению необходимость в бедных и несчастных замыканиях. Главным образом, он сетует на то, что так и не увидел после прочтения недавней статьи Брюса Тейта (Bruce Tate) какой-либо ценности в возможном введении замыканий в Java (последний использует для примеров Ruby):

Листинг 1. Простейшее замыкание.
3.times {puts "Inside the times method."}

Результаты:
Inside the times method.
Inside the times method.
Inside the times method.

Здесь times — метод объекта 3, он исполняет код замыкания три раза.
{puts "Inside the times method."}
и есть замыкание. Это анонимная функция, которая, будучи переданной методу times, выводит строку “Inside the times method.” на печать. Данный код является куда более ясным и простым, нежели его альтернатива с применением цикла for:

Листинг 2. Цикл без замыканий.
for i in 1..3
 puts "Inside the times method."
end

Даже я, увидев такое бледное и безжизненное введение в замыкания, с трудом увидел бы их настоящее значение. Первое, что приходит на ум при виде этих примеров: “Да, пожалуй тут есть некий тонкий оттенок”. Остальные примеры из статьи Брюса настолько же тривиальны, неясны и совсем не проливают света на предмет обсуждения.

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

Тем не менее, Эллиотт поднимает действительно важный вопрос: “Почему вы ненавидите цикл for”?

Вот вам распространенный пример:

double sum = 0;
for (int i = 0; i < array.length; i++) {
    sum += array[i];
}

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

Желаете доказательств? Пожалуйста, привожу вам следующий же пример из статьи Эллиотта:

String s = "";
for (int i = 0; i < args.length; i++) {
    s += array[i];
}

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

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

Как в этой ситуации могли бы помочь замыкания? Вот вам первый пример на Haskell:

total = sum array

Ну ладно-ладно, это было не совсем честно. Функция sum не использует замыканий — она определена через свертку (прим. переводчика: английский вариант — fold), которая, в свою очередь, использует замыкания.

total = foldl (+) 0 array

Вот вам второй пример применения замыканий:

s = concat array
s = foldr (++) [] array

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

Два приведенных выше примера показывают, как можно взять список и свернуть его в один элемент. В мире функционального программирования подобные операции называются свертками. Для своей работы свертка принимает функцию (замыкание), начальное значение (прим. переводчика: далее по тексту — “аккумулятор”) и посещает первый элемент списка. Замыкание применяется к аккумулятору и первому элементу списка, результат кладется в аккумулятор. Затем свертка применяет замыкание к обновленному значению аккумулятора и второму элементу списка. Этот процесс продолжается вплоть до конца списка, когда последнее значение аккумулятора и становится результатом применения свертки.

Вот наглядное объяснение:

s = foldl (+) 0       [1, 2, 3]
 = foldl (+) (0 + 1) [2, 3]
 = foldl (+) 1       [2, 3]
 = foldl (+) (1 + 2) [3]
 = foldl (+) 3       [3]
 = foldl (+) (3 + 3) []
 = foldl (+) 6       []
 = 6

Haskell предоставляет несколько функций-сверток: foldl начинает обработку списка с первого его элемента, и проходит последовательно до последнего элемента; foldr, напротив, начинает с последнего элемента, и движется по направлению к первому элементу. Есть и другие варианты, но эти два являются основными.

Разумеется, свертки являют собой очень примитивные операции, и было бы весьма неразумно выбросить за борт циклы for и заменить их различными fold-образными “заклинаниями”. Вместо этого, более высокоуровневые операции, такие как sum, prod и concat, определены в терминах сверток. Вы, в свою очередь, программируете уже в терминах данных высокоуровневых операций, получая более сжатый код, код, который гораздо проще прочесть, написать и понять.

Однако не каждый цикл представляет собой сворачивание списка в один элемент. Рассмотрим следующий пример:

for (int i = 0; i < array.length; i++) {
    array[i] *= 2;
}

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

new_array = map (*2) array

Функция map посещает каждый элемент списка, применяет к нему [элементу] переданное ей замыкание и создает из возвращаемых замыканием значений новый список. (Некоторые языки вместо создания нового списка замещают старые элементы исходного списка новыми). Такую операцию гораздо проще понять. sort делает нечто похожее, в том смысле, что она принимает список и возвращает новый список (или же модифицирует старый).

Третья разновидность циклов for — фильтрующие циклы. Рассмотрим пример:

int tmp[] = new int [nums.length];
int j = 0;
for (int i = 0; i < nums.length; i++) {
    if ((nums[i] % 2) == 1) {
        tmp[j] = nums[i];
        j++;
    }
}

Очень простой код, смысл которого, тем не менее, почти полностью погребен под излишней сложностью, к которой привело использование цикла for и двух индексов. Если фильтрация является такой же фундаментальной операцией, как fold и map, применение её должно быть настолько же простым, и, вообще говоря, так оно и есть:

odds = filter (\i -> (i `mod` 2) == 1) nums
odds = filter odd nums  -- более верно

Вот, собственно говоря, все недостатки цикла for: он объединяет в себе (по меньшей мере) три разновидности операций, и фокусирует внимание на второстепенной детали — прохождение последовательности (индексов) значений. Но на самом деле, свертывание, преобразование и фильтрация — это три различных способа обработки списка и они должны восприниматься по-разному. Использование замыканий в теле цикла позволяет гораздо нагляднее отделить что от как. Я мог бы писать анонимные функции всякий раз, когда мне нужно обработать список, или же использовать функции, написанные до меня (например odd, (+) или sqrt).

Если замыкания не являются эзотерическим инструментом, но глубоко встроены в язык и его стандартную библиотеку, нам не нужно возиться с этими низкоуровневыми операциями (прим. переводчика: здесь имеются в виду map, fold и filter). Вместо этого, мы можем создать высокоуровневые операции, с говорящими названиями, например sum или product.

Кроме того, работа с подобными терминами помогает гораздо легче понимать более сложные операции, такие как преобразование дерева (mapping a tree), фильтрация вектора (filtering a vector) или свертывание списка в хэш (прим. переводчика: не совсем понятно, что имел в виду автор под словами “folding a list into a hash” — сворачивание списка в одно-единственное значение, или функцию получения хэш-таблицы из списка кортежей, имеющуюся в стандартной библиотеке).

В конце концов Эллиотт поразводил руками на тему параллельного исполнения кода на нескольких ядрах, и посетовал на то, что код
3.times {...}
почти наверняка будет параллелиться хуже, чем аналогичный цикл for. Мне кажется, что он упускает из виду одно обстоятельство. Да, есть вычисления, которые должны выполняться последовательно, есть и такие, которые могут выполняться параллельно. Но отделение одних от других является для компилятора сложной задачей, если единственный используемый вами инструмент — цикл for. Если же вы можете разделить последовательные вычисления (то бишь foldl и foldr) и (возможно) параллельные (map и filter), то, тем самым, вы серьезно упрощаете задачу компилятора. Более того, вы даже можете явно запросить последовательную или параллельную версию map, если вы знаете природу обрабатываемых вами данных лучше, чем компилятор.

От переводчика


В тексте частенько проскальзывают упоминания замыканий. Если я правильно понимаю определение замыканий, то нужно сказать, что автор путает анонимные функции и замыкания — в приведенных им примерах отсутствуют функции, работающие с переменными из окружения, не указанными в списке параметров.
Перевод: Adam Turoff
Артём Гапченко @artemgapchenko
карма
43,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

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

  • +19
    Автор действительно путает замыкания с лямбдами.

    Основные плюсы функциональных циклов перед for, ИМХО:
    1. то, что правильно написанные функциональные циклы не имеют сайд-эффектов, в отличие от for, который на сайд-эффектах построен;
    2. декларативный стиль, мы описываем не то что мы хотим сделать, а то, что мы хотим получить в результате («хочу получить все нечетные элементы из списка» вместо «введем счетчик i, увеличим i на 1, возьмем i-й элемент последовательность...»).
    • 0
      Еще автор путает свертку и редукцию.
      • +2
        • –3
          что от чего?
          • 0
            fold от reduce
            • +5
              Вообще, я имел в виду что слово «свертка» было использовано неудачно.
              А что касается fold и reduce, то, насколько я помню, первое не требует ассоциативности используемого оператора, а второе — требует.
              • 0
                А как бы вы перевели стово fold в данном контексте? Там же так и написано sum = foldl (+)

                Не могли бы вы указать источник ваших сведений о reduce — я не смог быстро найти
                • 0
                  свёртка это сonvolution

                  • +1
                    Заметьте что вы решили обратную задачу — как перевести слово «свёртка» на английский, а не как «fold» на русский
                    • 0
                      Складка (как процесс складывания).
                      • 0
                        Слово «складывать» слишком двусмысленно. «Свертка», правда, тоже, но тут виноваты математики, занявшие его под перевод «convolution».
                    • 0
                      Если обратиться к
                      fprog.ru/2009/issue3/eugene-kirpichov-elements-of-functional-languages/
                      находим
                      «foldr — процедура правой списочной свертки»
  • –2
    Лично для меня главный вопрос: будет ли это работать быстрее. По-моему, лучше написать чуть больше кода, чем снижать производительность.
    • +8
      for конечно же быстрее.
      Но читабельность, надежность и скорость разработки часто имеют в разы большее значение чем производительность. Пользователь не заметит чем у вас перебираются 10 элементов в массиве.

      Поэтому не стоит заниматься preemptive optimization, чинить нужно только то, что реально тормозит и мешает жить пользователю.
      • +5
        вот от такой позиции мне порой не хватает четырехядерника
        • +18
          Кривые руки способны творить чудеса на любом языке и с любым железом.
          А при правильном использовании можно использовать приятные конструкции почти без потерь производительности.
          В задачах, с которыми я сталкивался (разного вида обработка текстов) при написании 5% кода на плюсах (вроде бы довольно ровного) и 95% — на питоне, код на плюсах занимает 95% процентов времени. При этом код на питоне, естественно, читать и модифицировать существенно легче. И даже если переписыванием его на плюсы удастся довести время выполнения до нуля — это не даст прироста производительности более чем на 5%.

          Просто некоторые «программисты» довольно слабо представляют себе, что такое разные структуры данных и зачем они нужны, какие алгоритмы где стоит применять и т.д.
          • 0
            Гнать таких в шею из нашей профессии!
            Понаехали тут… :-)
        • +3
          Если придерживаться противоположной позиции вам будет хватать 386, но не будет хватать функций ПО. В результате вы пользуетесь мозиллой или хромом, а не link и, фактически, выбираете текущий уровень компромиссов
      • 0
        [0..100000000000].map { |x| x.prime?}.take(50)

        Интересно посмотреть, как for будет работать быстрее, при этом будучи без костылей.
        • 0
          А если мы забудем про lazy evaluation и будем считать все полностью?

          И если бы в ruby был настоящий сишный for со счетчиком (а не итераторный foreach), то вариант с двумя счетчиками работал бы быстрее.

          Что-нибудь типа:

          for (var i = 0, j = 0; i < 100000000000 && j < 50; i++)
          {
          if (prime(i))
          {
          res[j] = i;
          j++;
          }
          }

          (псевдосишарп)
          • 0
            Да вопрос-то не в том, ленивое оно или нет, тем более, что для Ruby такая форма не подразумевает ленивости и мапит целиком весь диапазон (до известного недавного патча в 2.0), а в выразительности этого выражения (каламбур), и в отсутствии такой выразительности в Java.
            Учитывая возможности оптимизации в компиляторе, предположу, что будет очень сравнимо с for по скорости, а возможно, что и вообще байткод будет практически один и тот же.
            • +2
              Ок, я привык к дотнету в котором оно ленивое уже давно ;)

              Вообще в моем примере как раз показано, что можно написать такой for который будет быстрее неленивой функциональной реализации и аналогичной ленивой.
          • 0
            И если бы в ruby был настоящий сишный for со счетчиком (а не итераторный foreach), то вариант с двумя счетчиками работал бы быстрее

            А зачем? Вот человек правильно говорит: habrahabr.ru/post/140439/#comment_4693005
      • 0
        Я бы даже не стал утверждать, что for однозначно быстрее: тот же std::for_each из C++ может использовать loop unrolling. В остальном я с вами согласен.
        • +4
          Ну если начать обсуждать оптимизацию компилятором, то функциональные циклы часто используют хвостовую рекурсию, тоже разворачиваются на ура и тоже могут оказаться быстрее for.
          • 0
            Вот как раз хвостовая рекурсия на мой вкус в большинстве случаев гораздо менее читаема, чем обыкновенный for, т.к. при хвостовой рекурсии вы, фактически, «состояние», необходимое для просчетов, распихиваете в аргументы функции. Сейчас читаю SICP и каждый раз когда нужно делать хвостовую рекурсию у меня начинает ум за разум заходить, очень не хватает for i in… родного.
            • +1
              Это нормально, перестроиться после того как уже есть опыт императивного программирования очень сложно. Зато потом на стыке двух парадигм получается по-настоящему красивый код.

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

                Когда речь идёт о какой-нибудь свёртке или чём-нибудь подобном, действительно хвостовая рекурсия прекрасна (хотя обычно всё укладывается в map/reduce/filter). Но когда вычисление содержит счётчик, или же дополнительное хранилище «состояния» — вы начинаете гнуться под концепцию.

                Я считаю, что хвостовая рекурсия хороша тогда, когда каждый её шаг обособлен и может быть рассмотрен в изоляции, а не содержит кучу аргументов а ля counter, i, j.
            • +2
              Я не пропихиваю состояние, а определяю рекуррентное соотношение. И читать ее надо именно как рекуррентное соотношение.
      • +3
        Да ни фига не быстрее — все зависит от реализации.
        Вот сделал тестовую функцию (с сайдэффектами, чтоб компилятор ее не выбросил):
        #include int arr[] = {1, 2, 3};

        void
        test()
        {
        std::for_each(std::begin(arr), std::end(arr), [](int &elem)
        {
        elem++;
        });
        }

        cl /FAcs /c /O2 test.cpp

        и получаем В ТОЧНОСТИ такой же цикл, как и при "ручном" for:
        PUBLIC ?test@@YAXXZ ; test
        ; Function compile flags: /Ogtpy
        ; File c:\temp\test\test.cpp
        ; COMDAT ?test@@YAXXZ
        _TEXT SEGMENT
        ?test@@YAXXZ PROC ; test, COMDAT

        ; 8 : std::for_each(std::begin(arr), std::end(arr), [](int &elem)
        ; 9 : {
        ; 10 : elem++;
        ; 11 : });

        00000 48 8d 05 00 00
        00 00 lea rax, OFFSET FLAT:?arr@@3PAHA ; arr
        00007 48 8d 0d 0c 00
        00 00 lea rcx, OFFSET FLAT:?arr@@3PAHA+12
        0000e 66 90 npad 2
        $LL15@test:
        00010 ff 00 inc DWORD PTR [rax]
        00012 48 83 c0 04 add rax, 4
        00016 48 3b c1 cmp rax, rcx
        00019 75 f5 jne SHORT $LL15@test

        ; 12 : }

        0001b f3 c3 fatret 0
        ?test@@YAXXZ ENDP ; test


        Более того:
        #include int arr[] = {1, 2, 3};

        void
        test(int adder)
        {
        // Capture adder
        auto f = [=](int &elem)
        {
        elem += adder;
        };

        // Modify adder
        adder = 0;

        std::for_each(std::begin(arr), std::end(arr), f);
        }


        И получаем, что бы Вы думали
        PUBLIC ?test@@YAXH@Z ; test
        ; Function compile flags: /Ogtpy
        ; File c:\temp\test\test.cpp
        ; COMDAT ?test@@YAXH@Z
        _TEXT SEGMENT
        adder$ = 8
        ?test@@YAXH@Z PROC ; test, COMDAT

        ; 8 : // Capture adder
        ; 9 : auto f = [=](int &elem)
        ; 10 : {
        ; 11 : elem += adder;
        ; 12 : };
        ; 13 :
        ; 14 : // Modify adder
        ; 15 : adder = 0;
        ; 16 :
        ; 17 : std::for_each(std::begin(arr), std::end(arr), f);

        00000 48 8d 05 00 00
        00 00 lea rax, OFFSET FLAT:?arr@@3PAHA ; arr
        00007 48 8d 15 0c 00
        00 00 lea rdx, OFFSET FLAT:?arr@@3PAHA+12
        0000e 66 90 npad 2
        $LL17@test:
        00010 01 08 add DWORD PTR [rax], ecx
        00012 48 83 c0 04 add rax, 4
        00016 48 3b c2 cmp rax, rdx
        00019 75 f5 jne SHORT $LL17@test

        ; 18 : }

        0001b f3 c3 fatret 0
        ?test@@YAXH@Z ENDP ; test


        Серьезно, нет никаких причин, по которым декларативный стиль (с обобщенными алгоритмами и передаваемыми им фунаргами) должен быть медленнее императивного. Но задумайтесь сколько усилий потребуется Вам для императивного написания (а позже и понимания написаного) какого нибудь parallel_for_each или там task<>().then().then().then();
        • 0
          Я там дальше примерно так и написал ;))
        • +1
          Хех, нужно source вместо code. Ну да фиг с ним — идея понятна.
          • +4
            На всякий случай перепишу для повышения читаемости:
            #include <algorithm>
            
            int arr[] = {1, 2, 3};
            
            void
            test()
            {
            	std::for_each(std::begin(arr), std::end(arr), [](int &elem)
            	{
            		elem++;
            	});
            }
            


            Компилируется в

            PUBLIC	?test@@YAXXZ					; test
            ; Function compile flags: /Ogtpy
            ; File c:\temp\test\test.cpp
            ;	COMDAT ?test@@YAXXZ
            _TEXT	SEGMENT
            ?test@@YAXXZ PROC					; test, COMDAT
            
            ; 8    : 	std::for_each(std::begin(arr), std::end(arr), [](int &elem)
            ; 9    : 	{
            ; 10   : 		elem++;
            ; 11   : 	});
            
              00000	48 8d 05 00 00
            	00 00		 lea	 rax, OFFSET FLAT:?arr@@3PAHA ; arr
              00007	48 8d 0d 0c 00
            	00 00		 lea	 rcx, OFFSET FLAT:?arr@@3PAHA+12
              0000e	66 90		 npad	 2
            $LL15@test:
              00010	ff 00		 inc	 DWORD PTR [rax]
              00012	48 83 c0 04	 add	 rax, 4
              00016	48 3b c1	 cmp	 rax, rcx
              00019	75 f5		 jne	 SHORT $LL15@test
            
            ; 12   : }
            
              0001b	f3 c3		 fatret	 0
            ?test@@YAXXZ ENDP					; test
            


            А

            #include <algorithm>
            
            int arr[] = {1, 2, 3};
            
            void
            test(int adder)
            {
            	// Capture adder
            	auto f = [=](int &elem)
            	{
            		elem += adder;
            	};
            
            	// Modify adder
            	adder = 0;
            
            	std::for_each(std::begin(arr), std::end(arr), f);
            }
            


            в

            PUBLIC	?test@@YAXH@Z					; test
            ; Function compile flags: /Ogtpy
            ; File c:\temp\test\test.cpp
            ;	COMDAT ?test@@YAXH@Z
            _TEXT	SEGMENT
            adder$ = 8
            ?test@@YAXH@Z PROC					; test, COMDAT
            
            ; 8    : 	// Capture adder
            ; 9    : 	auto f = [=](int &elem)
            ; 10   : 	{
            ; 11   : 		elem += adder;
            ; 12   : 	};
            ; 13   : 
            ; 14   : 	// Modify adder
            ; 15   : 	adder = 0;
            ; 16   : 
            ; 17   : 	std::for_each(std::begin(arr), std::end(arr), f);
            
              00000	48 8d 05 00 00
            	00 00		 lea	 rax, OFFSET FLAT:?arr@@3PAHA ; arr
              00007	48 8d 15 0c 00
            	00 00		 lea	 rdx, OFFSET FLAT:?arr@@3PAHA+12
              0000e	66 90		 npad	 2
            $LL17@test:
              00010	01 08		 add	 DWORD PTR [rax], ecx
              00012	48 83 c0 04	 add	 rax, 4
              00016	48 3b c2	 cmp	 rax, rdx
              00019	75 f5		 jne	 SHORT $LL17@test
            
            ; 18   : }
            
              0001b	f3 c3		 fatret	 0
            ?test@@YAXH@Z ENDP					; test
            


    • +7
      В этом месте можно бесконечно впадать в крайности вплоть до того момента, когда производительности асма оказывается недостаточно
    • +4
      с одной стороны любой компромисс зависит от условий — когд-то лучше производительность, когда-то лучше меньше кода

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

      Что касается того, что лучше быстрее программа или меньше кода. То обычно лучше меньше кода, вернее лучше, когда код легче читается, растет скорость разработки и уменьшается количество ошибок, т.к. если программа работает недостаточно быстро, то это легко решается покупкой более мощного компьютера, либо оптимизацией конкретного места в программе. А вот плохой код в дальнейшем сложнее поддерживать, ошибки портят жизнь и т.д. и т.п. и эти места покупкой железяки не исправить.
      Как-то так :)
    • +6
      Cудя по Вашему профилю и статьям, Вы работаете в областях, требующих крайне высокой производительности. В них действительно сложные высокоуровневые средства могут дать критическое замедление.
      Но в огромном числе задач скорость разработки и отсутствие ошибок важнее.
      Пользователь едва ли заметит, что на его простаивающем iN выполнится не тысяча инструкций, а десять тысяч. И гуглу проще купить лишний сервер, чем повышать шансы на наличие в коде ошибки, приводящей к уязвимости.
      Естественно, везде должен быть компромисс.
    • 0
      Measure!
  • 0
    А при чём тут for?

    Раз уж примеры на плюсах, то
    total = sum array
    развернутая функция с внутренним for циклом.
    • 0
      В total нет внутреннего for цикла, там применяется свертка, т.е. конструкция вида foldl1 (+) list. Сложение происходит не итеративно, а рекурсивно.
      • –5
        Ещё «лучше». Боязнь for-а лучше дать в жертву производительности.
        Вы уж извините, но придирки в статье (знаю, что перевод) высосаны из пальца.
      • 0
        Ну фактически там цикл есть, просто на этом уровне он скрыт от программиста, и проявляется только в ассемблерном коде.
        • 0
          Если транслятор оптимизирует.
          • 0
            Если транслятор не оптимизирует, то тут уже ни о каких преимуществах перед for речи не идет.
            • 0
              Семантика/лёгкость чтения/вероятность допустить ошибку тоже преимущества. Но вот именно в циклах (явных в коде или ещё как) весьма сомнительное. Особенно если речь идёт о «прогулках» не по десяти элементам, а много больше, а тело «прогулки» относительно лёгкое.
  • 0
    По существу мне сказать нечего (кроме того что мне не нравится принцип TIMTOWTDI в групповой разработке)
    но высказывание
    Потому что более краткая форма записи ведет к куда как меньшей вероятности возникновения ошибок в коде, и, когда они все-таки появляются, их (в основном) гораздо проще отловить.

    спорно.
    • +3
      Да ладно Вам. Что может быть непонятного в таком коде:
      print join(" ",grep{$_==2?1:$_
      • +8
        Парсер — лох. А я всегда буду нажимать предпросмотр:)
        print join(" ",grep{$_==2?1:$_<2||!($_%2)?0:do{for($b=1,$a=3;$a<=sqrt$_;$a+=2){do{$b=0;last}if!($_%$a)}$b}}(shift..shift)),"\n"
        • 0
          Краткость измеряется не в символах. Конкретно этот код никак не короче приведённого в статье на 4 строки (алгоритм разный, я понимаю, я просто о том, что хоть там и 4 строки, но он всё же короче)
          • 0
            Только этот код делает чуть более сложную работу — он ищет все простые числа в диапазоне от первого аргумента до второго.
            • +5
              Это неважно, я лишь указал, что краткость надо мерить не в символах.
        • 0
          мне этот текст непонятен просто потому, что я не знаю перл — это раз, а во-вторых, возможно имена неудачные
          • 0
            Имена переменных и отсутвие отступов.
            • 0
              про отсутпы тоже подумал
          • +1
            Тем не менее очень короткий и совершенно непонятный код.
            Ну и про патч Бармина не забываем:)
            $??s:;s:s;;$?::s;;=]=>%-{
    • +9
      Автор ошибается в причинах меньшего количества ошибок.
      Реально ошибок меньше не из-за того что написано короче, а из-за того что не используются дополнительные состояния, которые разработчик может как-либо менять.
      • 0
        Более короткий кусок кода может соделжать меньше дублирования. Например, в приведеном куске кода про args, источник ошибки — то, что название массива повторяется два раза и можно спутать.
        • +3
          Это тоже, но не сильно важно, если вы используете современную IDE c code competion.

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

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

          т. н. «чистая функция» имеет всегда как бы только одно состояние и после привыкания к функциональным парадигмам и читать и анализировать ее гораздо проще.
          • 0
            code completion это хорошо, но он не помогает читать программу, он может закомплитить другую переменную так же успешно как и ту же самую. Я согласен про аргумент с состоянием, но именно в этом случае источник ошибки скорее не введение переменной, а то, что конструкция требует имени массива два раза.
            • 0
              С учетом того, что цикл идиоматический, то да. А если не идиоматический, то, имхо, вероятность ошибиться с инициализатором или условием останова цикла больше.

              А насчет stateless — его преимущества гораздо лучше видны не на таких коротеньких примерах, а на сложных преобразованиях последовательностей сложных структур данных.
            • 0
              Чем больше сущностей, тем больше ошибок, нет?
              • 0
                Тем больше возможностей исправить ошибки проектирования. Потому что дополнительные сущности повышают гибкость.
                Я очень люблю объединять сущности, имеющие разное семантическое назначение по принципу «они почти похожи, почему бы не воспользоваться одной и той же структурой — ведь мир должен быть устроен разумно, а значит, компактно». А потом оказывается, что я не могу поменять параметры одной из них, не разрушив свойств другой. Приходится их расщеплять. Противно, а что делать.
                • 0
                  Потому что нарушаете Single responsibility principle.
                  • 0
                    Знаю. Но он начинает конфликтовать с DRY.
                    • +1
                      Значит надо строить иерархии или вводить примеси.
                      • +1
                        И надеяться, что компилятор всю математику хорошо заинлайнит, и я не потеряю в производительности. Так и приходится делать :(
                        • +1
                          Если важна производительность — то тогда уже надо искать компромисс. Согласен, это проблема.
              • 0
                и я про это
              • 0
                Зависит от правильности декомпозиции.
        • 0
          чем плох вариант с foreach? Там тоже дублирования нет.
          • 0
            Для array_walk — ничем, даже лучше во всём (если тело цикла два раза не используется). Для map, reduce и т. п. — видно сначала, что перебор, но семантика всего оператора лежит ниже и требует чтения кода. Сначала видим, что что-то происходит для каждого элемента, а лишь потом видим, что конкретно с ним происходит… Акцент, как правильно замечено, не на том, что нужно. Абстрагироваться не даёт. Нельзя остановиться на уровне: «эта конструкция берёт массив и возвращает скаляр, массив и т. п.». Нарушается инкапсуляция, если угодно.
          • 0
            дам два раза i в отличии от foldl (+)
  • +12
    Да ну, приведённые примеры с map/fold/filter очень и очень хеллоуворлдные. На каких-то простых примерах к ним всё действительно сводится. А вот на практике… Ну вот скажем, что делать, если наша логика преобразования списка предполагает знание индекса элемента? Плодить систему из zip/unzip? Я так и делал, когда писал на haskell в образовательных целях библиотечку для линейной алгебры, и мне нужно было знать номер строки/столбца при обходе матриц. Надо сказать, что в целом получилось компактнее, но не сказал бы, что сильно читабельнее или понятнее, чем на Java.

    Вообще, секта функциональщиков, как мне кажется, чего-то упускает. Обычно приводятся такие вот лаконичные примеры на haskell. Мол, парсер языка в три строчки, вычислитель выражений в 5 строчек. А вот вообще программа почти один-в-один совпала с денотационной семантикой. Как круто! Вот только эти примеры не выдерживают никакой критики для практического использования. Писал я как-то компилятор на haskell. И вот на чём споткнулся при написании банально парсера. Примеры, которые приводятся, принимают правильную строку и вообще не принимают ошибочную. Реальные парсеры умеют восстанавливаться после ошибки и предлагать пользователю вменяемый трейс. Когда я пытался сделать что-то подобное на haskell, я столкнулся с тем, что надо постоянно таскать кучу всякой фигни (фактически, состояние) в функциях. Тогда я, недолго думая, завернул это дело в монаду (получился своеобразный parser monad). Ну что сказать? Получилось несколько компактнее, чем на Java. Но вот понятнее ли? Вы представляете, надо было сделать свою монаду! А человек, читающий код, должен ещё и разобраться с этим. Я ни в коем случае не говорю, что Haskell — плохой язык. Haskell хорош, но он далеко не настолько лучше, чем Java (или любого другого императивного языка), чем это принято преподносить со стороны евангелистов Java.
    • 0
      Надо понимать, что Haskell больше исследовательский язык и разрабатывается как pure functional чисто из исследовательских целей.

      В реальном мире правильный подход — гибридный.
    • +2
      zipWith foo [0..?

      80% примеров с map/fold/filter именно такие. Программист сейчас должен компоновать такие большие куски уже написанного, а не выдумывать BLAS. Для 20% можно и цикл написать.
    • +1
      Если не бояться слова «монада», то это равносильно написанию одного класса. Специализированные функции для Parser monad, плюс instance Monad Parser в 2 тривиальных функции. Это правда сложно?
      • +6
        Я не боюсь слова монада. Но вот помнится в своё время, когда я столкнулся с ООП, я понял его концепции на интуитивном уровне за пару дней. С монадами я возился месяца 2, причём, опять же, не во в всех их проявлениях (не забываем, что бывают не только state monad, но и list monad, maybe monad, etc). И так повсюду: если в императивных языках с какой-либо новой концепцией можно разобраться, просто глядя в код (который длинный), то в Haskell можно смотреть на три строчки, восхищаться, как оно изящно и красиво сделано, но не понимать применённой автором магии, пока не прочтёшь его статью с кучей математики и тщательно не переваришь эту статью в голове.
        • +1
          Вот именно, что сам ваш подход и обеспечил такую сложность понимания монад.
          Это как сказать, что есть main classes, object classes, util classes, helper classes — и еще целую кучу напридумывать можно. Монады — это не list monad, maybe monad, это просто монады. А вышеперечисленное — просто конкретные монады, которые, как и конкретные классы, которые наследуют общую концепцию класса, тоже наследуют общую концепцию монады, а в сущности различаются, как, опять же, и конкретные классы только областью применения.
          • 0
            А общая концепция монады — это из теории категорий. Я пытался читать статьи, где объяснялись математические свойства монад и т.п. Чисто теоретически, всё похоже на обычную алгебру, но вот как эту алгебру подружить с реальной задачей? У меня так и не возникло понимания, как видя конкретную задачу и зная свойства монад, можно понять две вещи: нужны ли здесь монады и как их тут применить. Но в итоге пришлось смириться и просто понять, как можно использовать конкретные монады, т.е. state monad или list monad. Но вот придумать свою монаду — это для меня задача неподъёмная.

            Кстати, монады — это цветочки. Есть стрелки. А ещё бананы с линзами. И прочее, и прочее.
            • 0
              Я на комментарий ниже ответил, потом прочитал ваш — и оказалось, что вам я тоже ответил там.
              Плюс — как же неподьемная, вы же придумали свою монаду для парсера?
          • 0
            Боюсь спросить, но каков-же «правильный» подход, обеспечивающий низкую сложность понимания?
            • 0
              Ну я математик, так что мне через теоркат, а программистам — класс «Monad» это просто «интерфейс». То есть все монады просто наследуют некий интерфейс «Monad». Вот и все.
              • 0
                Не понял мысль. Вы предполагаете, что замена одного слова другим радикально упростит процесс обучения?
                • –1
                  Ну, интерфейс же — это всем известная вещь. Ну то есть как минимум всем ООП-программистам.
        • 0
          Математика в этом смысле ещё суровее, ну а что поделать.
          • +2
            Я знаю, сам физмат заканчивал. И всё-таки, программирование решает практические задачи, не надо задирать порог вхождения до небес. У математики просто задачи другие — там можно до бесконечности вертеть формулы, даже не представляя себе конечного результата и его практического применения. Вон, например, бесконечномерные линейные пространства вертели в своё время прикола ради, не понимая, зачем они нужны. А потом, когда открыли квантовую механику, оказалось, что её в матричной форме удобно записывать. Программирование всё-таки ближе к инженерии. Там сроки и результат должны быть более детерминированными. И не надо ещё из виду упускать то, кого берут в программисты. Некоторые Java-программисты даже не совсем понимают, что такое лямбды и зачем они нужны. А вы про монады им…

            И вообще, ну вот перейдут сейчас все дружно на Haskell и что дальше? Будет меньше кода, или он будет быстрее читаться/писаться? Часто приводят в пример, как на Haskell изящно записывается быстрая сортировка. Вот только нифига она не быстрая, и вообще в функциональных языках лучше сортировать слиянием. А она уже подлиннее будет той волшебной формулы на Haskell.
            • +1
              Вот я тоже не люблю, когда коммерческое программирование пытаются предоставить как искусство или науку. А это почти чистая инжерения.
            • +1
              Если все перейдут на Haskell, то и на нём навертят такого, что без слёз не взглянешь, это понятно. И что инструмент лучше попроще, чтоб не надо было на него умника искать, а взять десяток средних.

              Я лишь говорил о том, что для программиста на Haskell, написать свою монаду даже проще, чем написать класс. Чем ближе к матану, тем мощнее абстракции. Порог, конечно, значительно повышается, но если его преодолеть, это с лихвой окупится.
    • +1
      Согласен. В примерах обычно приводятся простые случаи.
      Я некоторое время писал на питоне, и мне очень нравилось иногда использовать функциональный стиль, но было важно не перестараться, ведь в сложных случаях цикл может быть даже понятнее, и его проще отлаживать.
      И мне нравится, что в питоне можно использовать оба стиля: императивный и функциональный.
    • +4
      Вы представляете, надо было сделать свою монаду!

      А для программиста на хаскеле это вполне естественный порядок вещей. Есть состояние — делаем свою монаду. Ну это после поиска уже существующей по запросам.
      Я вот после хаскеля на шарпе пишу снова, тоже моге сказать что-то типа:
      Вы представляете, на каждый чих нужно свой класс писать!
      • 0
        Зато классы проще. А на монадах такую магию творят, что башку сносит конкретно!
        • 0
          Классы ничуть не проще. Просто привычнее современным ООП и процедурным программистам.
      • 0
        И какой вариант цикла короче на шарпе: императивный или с лямбдами? Когда использовал aggregate, получилось чуточку длиннее.
    • +1
      Вы исходите из своего опыта, но скажу я вам, он неполон. Вы говорите, что надо таскать кучу фигни с собой, из чего делаете вывод о всем Haskell. Это в общем случае неверно. Да чего далеко ходить? Вот мой парсер телефонного трафика, программа используется в предприятии. Есть там и задача с индексацией списка. Делается элементарно, и никакого состояния с собой не нужно иметь. Просто надо уметь задачу обработки данных решать функциональными методами. Тогда и монада может не потребоваться. Пожалуйста, код:

      -- Аргумент 1 - аккумулятор
      -- Аргумент 2 - список нужных индексов
      -- Аргумент 3 - список сырых полей
      collectFields :: Int -> [Int] -> [String] -> [String]
      collectFields _ _ [] = []
      collectFields idx fis (s:ss) | idx `elem` fis = s : collectFields (idx+1) fis ss
      collectFields idx fis (s:ss) | otherwise = collectFields (idx+1) fis ss
      • +1
        Мы тут обсуждаем полезность функций типа filter, а вы херачите явную рекурсию с аккумулятором, что ни чем не лучше цикла, критикуемого в статье.
      • 0
        Риквестирую перенос этого камента в шапку статьи. Как пример превосходства рекурсии над циклом for!
        • 0
          Не очень хороший код — можно было воспользоваться стандартной функцией:

          collectFields :: Int -> [Int] -> [String] -> [String]
          collectFields idx fis lst = snd $ filter (\(n,_) -> x `elem` fis) $ zip [idx..] ss
          • 0
            Упс, snd $ unzip $ filter…
          • 0
            Определённо лучше. Что он делает мне уже не понять. Good for me.
          • +2
            collect :: [Int] -> [String] -> [String]
            collect is ss = [s | (i, s) <- zip [0..] ss, i `elem` is]

            • 0
              Да, вы только часть работы вынесли наружу, может автор из принципа так не хотел.
    • 0
      > Ну вот скажем, что делать, если наша логика преобразования списка предполагает знание индекса элемента?

      Значит нужен не список, а массив или словарь, не?
      • 0
        Не нужен, ибо задачи поиска элемента по заданному индексу не ставилось. Я уже сказал: используются zip/unzip, zipWith возможность в аргументе лямбды или list comprehension'а использовать паттерн-матчинг (точнее, написать кортеж, что является частным случаем). Но там всё уже будет не так изящно, и получится, что императивный стиль не сильно проиграет в выразительности.
        • 0
          Вот простенькая обертка решает проблему «неизящности»:
          filterIndexed :: ((Int,a) -> Bool) -> [a] -> [a]
          filterIndexed fun lst = filter fun $ zip [0..] ss

          Хотя я вообще думаю, что стандартной функции такой нет только потому, что хаскелисты не считают такой подход неизящным, я вот вроде не только хаскелист, но тоже считаю решение с зипами вполне замечательно читабельным.
          • 0
            Чето походу я устал уже…
            filterIndexed fun lst = snd $ unzip $ filter fun $ zip [0..] ss
  • +7
    Обожаю цикл for! Честно :)
  • 0
    Охренительно (не)переведена фраза «Personally I find the latter example simpler, clearer, and easier to understand». Переводил предвзятый хаскельщик.
    • 0
      (не)переведена фраза «Personally I find the latter example simpler, clearer, and easier to understand».

      Вы точно нашли эту фразу именно в той статье, с которой я делал перевод? Потому что я сейчас проверил оригинал и ее там не нашел.
    • +1
      А, виноват. Это не тот перевод. Там оказывается целая дискуссия, и Adam Turoff — это третий в цепочке полемизирующих авторов.
  • +3
    вам не нравится конструкция

    double sum = 0;
    for (int i = 0; i < array.length; i++) {
    sum += array[i];
    }

    ну сделайте в коде
    double sum = Util.arraySum ( array ); // и еще камент можно дописать, что это сумма элементов

    а над статическим методом arraySum из класса Util можно как угодно потом изгалаяться — суммируя элементы хоть с начала, хоть с конца, хоть еще каким-то образом в зависимости от условий на этапе компиляции или в рантайме.
    • 0
      Проблема в том, что он умеет делать только sum. И если нужен *2, то нужен ещё один статический метод в Util, который уже сам по себе антипаттерн.
      Возможно сделать и каскадирование, если каждый из таких методов будет возвращать нечто вроде LazyCollection, а в конце будет вызываться result() или calculate(). Но это уродство и не имеет никаких преимуществ.
    • 0
      а если хочу не просто sum, а сум с фильтрацией по лямбде?
    • 0
      Нужен ещё интерфейс «лямбда», для создания разных функций. Тогда действительно нет проблем с реализацией паттернов функционального программирования на джаве.
  • 0
    Хм. Поставил возможные for'ы на макросы в нетбинсе и как-то проблем не испытываю.
    «for (var i = 0; i < .length; i++){» insert-break
    «for (var i = .length; i--;){» insert-break
  • 0
    Задумался. А как быть с такой довольно распространенной задачей, как разбить массив на два и более? Пускай в одном должны быть чётные элементы, в другом нечётные. Естественно без многократного прохода по массиву.
    • +1
      По-моему, все просто:

      import Data.List
      
      a = [1..100]
      
      res = partition odd a
    • 0
      Еще вариант — через свертку:

      a = [1..100]
      
      f :: Int -> ([Int], [Int]) -> ([Int], [Int])
      f x (oddXs, evenXs) | odd x     = (x : oddXs, evenXs)
                          | otherwise = (x : oddXs, x : evenXs)
      
      res = foldr f ([], []) a
      
      • +1
        Пардон, во втором теле функции ошибка (говорили же мне, копипаст — плохо!):

        f :: Int -> ([Int], [Int]) -> ([Int], [Int])
        f x (oddXs, evenXs) | odd x     = (x : oddXs, evenXs)
                            | otherwise = (oddXs, x : evenXs)
        
  • 0
    Я ненавижу цикл for за семантическое излишество и необходимость плодить сущности, но он в 3 раза эффективнее array_reduce на PHP (a foreach — в 5 эффективнее последнего). Всё-таки вызов функции более тяжелая операция, чем цикл. Может для тяжелых тел им можно и пренебречь, но вот для простых задач (вроде суммирование элементов) оверхид значителен.
    • –1
      Правильный компилятор функцию, которые вызывается слишком часто, заинлайнит. Или PHP до сих пор интерпретируется?
      • 0
        Транслирует в байт-код, но, видимо, без оптимизаций, а «as is».
    • 0
      Функциональные языки (и вообще стиль) на неймановской архитектуре машины всегда будут уступать по производительности выполнения кода императивным. Особенно, если выполнять код буквально, без оптимизатора, который умеет проводить аналогии между функциональными и императивными конструкциями.
      • 0
        Поинтересуйтесь тем, что такое «шитый код» и почему он иногда может быть быстрее обычного. Это если не учитывать разворачивание функциональных конструкций в императивные.
        • 0
          А какое отношение имеет шитый код к «противостоянию» процедурных и функциональных языков? Мне помнится «шитый код» был в таком языке как FORTH (сам лично его реализовывал для i8080). Он ни к функциональным, ни к процедурным не относится.
          • 0
            Тем, что функциональщина очень легко и прозрачно компилируется именно в шитый код.
            • 0
              В принципе да, если отбросить режимы трансляции и прочее, то шитый код выглядит как функциональный язык — ссылка на функцию обработки и параметры-функции, грубо говоря.

              Обратное неверно :) Хотя и заметил что-то общее с FORTH, когда начал «функциональщину» изучать. В смысле отличающее от «процедурщины» (пускай она даже ООП, с плюсами). Декларативный подход общее. Все знакомые мне чисто функциональные языки — декларативные почему-то :) Обратное неверно. :)
              • 0
                Ну, в принципе, ничто не мешает составить правильный словарь и писать на Форте хоть в функциональном, хоть в каком другом стиле ;)))

                Но в базовой идеологии, да, много очень общего.
                • 0
                  В чисто императивном на Форте писать очень сложно. Он нарушает все принципы Форта. С другой стороны слова Форта чистыми функциями назвать никак нельзя, если сильно не стараться. И то, параметры в стэк надо засовывать до вызова, а не после.
        • 0
          Я знаю, что он такое, но это «иногда» исключительно колличественная штука, никак не качественная. То есть уничтожается увеличением памяти на машине. Да, есть еще и размер кэша процессора, но это все мелочи жизни. И мой комментарий как раз и учитывал неразворачивание инструкций.
          • 0
            Согласен. Но это же не категоричное «всегда» как в вашем комментарии ;)
  • 0
    90% того, что хотелось бы делать на замыканиях, можно сделать на макросах с хорошей реализацией. (Хорошая — это не такая, как в Си). Но, к сожалению, макросы не в каждый язык безболезненно добавишь (например, в ту же Java).
    • 0
      Можно, а нужно? Хотя с другой стороны всё к ассемблерной «императивщине» сводится хоть на CISC, хоть на RISC.
    • 0
      Не знаю, в макросах очень легко ошибиться. И то, что их нет в Java по-моему хорошо.
  • +1
    Хм. Не понятно. Почему иметь три разные конструкции и выстраивать между ними мозговыворачивательные переходники лучше, чем иметь универсальный for, который легко подстраивать под конкретную ситуацию?
    • 0
      Ваш вопрос эквивалентен:

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


      Затем же, зачем делают обёртки над одной универсальной функцией.
      Затем же, зачем в CAD'ах есть кнопка «прямоугольник», хотя это просто полилиния замктуная. И даже для Arc делают аж 3-4 кнопки, а не одну, которой можно создать любой Arc.
      Затем же, зачем пишут (пример условный)
      square(point(0, 0), 20);
      вместо
      polyline(point(0,0), point(20,0), point(20,20), point(0, 20));

      Чтобы было проще читать код. По имени map/filter и прочим сразу ясно, что делается. По for/foldr — набившие руку в этом деле, конечно, достаточно быстро разбирают, но тем не менее напрямую использовать foldr — дурной тон. Зачем лишние усложнения?

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