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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Я считаю, что хвостовая рекурсия хороша тогда, когда каждый её шаг обособлен и может быть рассмотрен в изоляции, а не содержит кучу аргументов а ля counter, i, j.
Я не пропихиваю состояние, а определяю рекуррентное соотношение. И читать ее надо именно как рекуррентное соотношение.
Да ни фига не быстрее — все зависит от реализации.
Вот сделал тестовую функцию (с сайдэффектами, чтоб компилятор ее не выбросил):
#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();
Я там дальше примерно так и написал ;))
Хех, нужно source вместо code. Ну да фиг с ним — идея понятна.
На всякий случай перепишу для повышения читаемости:
#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


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

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

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

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

спорно.
Да ладно Вам. Что может быть непонятного в таком коде:
print join(" ",grep{$_==2?1:$_
Парсер — лох. А я всегда буду нажимать предпросмотр:)
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"
Краткость измеряется не в символах. Конкретно этот код никак не короче приведённого в статье на 4 строки (алгоритм разный, я понимаю, я просто о том, что хоть там и 4 строки, но он всё же короче)
Только этот код делает чуть более сложную работу — он ищет все простые числа в диапазоне от первого аргумента до второго.
Это неважно, я лишь указал, что краткость надо мерить не в символах.
мне этот текст непонятен просто потому, что я не знаю перл — это раз, а во-вторых, возможно имена неудачные
Имена переменных и отсутвие отступов.
про отсутпы тоже подумал
Тем не менее очень короткий и совершенно непонятный код.
Ну и про патч Бармина не забываем:)
$??s:;s:s;;$?::s;;=]=>%-{
Автор ошибается в причинах меньшего количества ошибок.
Реально ошибок меньше не из-за того что написано короче, а из-за того что не используются дополнительные состояния, которые разработчик может как-либо менять.
Более короткий кусок кода может соделжать меньше дублирования. Например, в приведеном куске кода про args, источник ошибки — то, что название массива повторяется два раза и можно спутать.
Это тоже, но не сильно важно, если вы используете современную IDE c code competion.

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

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

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

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

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

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

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

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

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

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

А для программиста на хаскеле это вполне естественный порядок вещей. Есть состояние — делаем свою монаду. Ну это после поиска уже существующей по запросам.
Я вот после хаскеля на шарпе пишу снова, тоже моге сказать что-то типа:
Вы представляете, на каждый чих нужно свой класс писать!
Зато классы проще. А на монадах такую магию творят, что башку сносит конкретно!
Классы ничуть не проще. Просто привычнее современным ООП и процедурным программистам.
И какой вариант цикла короче на шарпе: императивный или с лямбдами? Когда использовал aggregate, получилось чуточку длиннее.
Вы исходите из своего опыта, но скажу я вам, он неполон. Вы говорите, что надо таскать кучу фигни с собой, из чего делаете вывод о всем 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
Мы тут обсуждаем полезность функций типа filter, а вы херачите явную рекурсию с аккумулятором, что ни чем не лучше цикла, критикуемого в статье.
Риквестирую перенос этого камента в шапку статьи. Как пример превосходства рекурсии над циклом for!
Не очень хороший код — можно было воспользоваться стандартной функцией:

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

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

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

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

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

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

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

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

import Data.List

a = [1..100]

res = partition odd a
Еще вариант — через свертку:

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
Пардон, во втором теле функции ошибка (говорили же мне, копипаст — плохо!):

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

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

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

Хм. Не понятно. Почему иметь три разные конструкции и выстраивать между ними мозговыворачивательные переходники лучше, чем иметь универсальный 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 — дурной тон. Зачем лишние усложнения?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории