Здравствуйте, Бойко!

Спасибо за ценные, интересные аргументы.

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

> Более того, вы пишете, что возможность самоцитирования (и цитирования 
> охватывающей функции) для безымянных функций «нужна редко»…
В предыдущем письме вы привели пример кода на JavaScript, тем самым подтолкнув 
меня в сторону императивной парадигмы. А там рекурсия бывает редко, а значит, 
раз потребовалось рекурсия, значит задача достаточно сложна, циклами её не 
решить. И для подобной задачи имеет смысл реализовывать именно именованную 
функцию (иначе может быть вообще не понятно). В рамках функциональной 
парадигмы, где циклов нет, действительно, напрягает именовать каждую функцию, 
которая по смыслу эквивалента while или for.

> Впрочем, по поводу утверждения «нужна редко», вспомнил о цикле со 
> след-условием (do-while или repeat-until).  Он часто используется?…
(В Python’е, кстати, его нет. Именно потому, что он используется редко — это 
следует из идеологии языка. Но это отступление.)

> А зачем нужен while в языке C и подобных, когда он — всего лишь частный 
> случай for, с тяжелыми ограничениями?
> Насколько часто нужно написать цикл без ни вводной, ни обновляющей частей?  
> По-моему, очень редко.
Я посмотрел несколько программ на Си/Си++.
Рантайм компилятора Простого Рефала и стандартная библиотека:
>git grep -w for | bash -c "wc -l"
45
>git grep -w while | bash -c "wc -l"
28

Коммерческий проект, над которым я тружусь на второй работе (я не только 
преподаю):
>git grep -w for *.[ch] | bash -c "wc -l"
509
>git grep -w while *.[ch] | bash -c "wc -l"
183
Поиск осуществлялся только по исходникам *.h и *.c.

Интерпретатор Рефала/2 Стеллецкого — while не используется, for используется 
так:
>git grep -w for
refalb.cpp: for(;;)
refalb.cpp:  for(i=0;i<j;i++) if(fputc(X[LX+2000+i],F)==EOF) goto labew;
swfmk.c: for(i=LX;;i++)
swfmk.c: for(i=LX;;i++)
swfmk.c: for(i=LX;cc!=REFTM.next1;i++,cc=sl(cc)) X[i]=(char) par(cc);
swfmk.c: for(i=LX;cc!=REFTM.next1;i++,cc=sl(cc)) X[i]=(char) par(cc);
swfmk.c: for(cc=sl(i);cc!=REFTM.next1;cc=sl(cc),j++) X[LX+2000+j]=(char) 
par(cc);
swfmk.c: for(;G0!=REFTM.gkop;)
swfmk.c:  for(i=sl(REFTM.prev1),cc=sl(G0);i!=REFTM.next1;i=sl(i),cc=sl(cc))
swfmk.c: for(;G0!=REFTM.gkop;)
swfmk.c:  for(i=sl(REFTM.prev1),cc=sl(G0);i!=REFTM.next1;i=sl(i),cc=sl(cc))
swfmk.c:   {for(cc=sl(cc);cc!=G2;cc=sl(cc))
swfmk.c: for(i=sl(REFTM.prev1);i!=REFTM.next1;i=sl(i))
swfmk.c: for(i=0;s[i]!=0;i++)
swfmk.c:  for(cc=sl(j);cc!=j;cc=sl(cc))
swfysb.c: for(i=0;i<arg;i++)
swfysb.c: for(i=arg;i>0;i--)
swfysb.c:  for(;;)
swfysb.c:  for(;;)
swfysb.c: {for(k=spG1;k!=sl(spG2);k=sl(k))
swfysb.c:   for(cc=0,i=0;i<j;i++)
swfysb.c: for(k=0;k<arg;k++)
swfysb.c: for(;pr(ast)!=TE[arg];ast=sl(ast))
swrefmf.cpp: {for(i=a;i!=sl(b);i=sl(i))
swrefmf.cpp: {for(i=a,n=0;i!=sl(b) && n<c;i=sl(i),n++)
swrefmf.cpp: for(i=4+1;i<n+4;i++) sv(i-1,i)
swrefmf.cpp: for(;;REFTM.nsh++)
Обнаружилась пара циклов while, записанных как for(;…;) и пара бесконечных 
циклов. А ещё в исходниках можно найти один цикл на goto :-).

Исходные тексты компилятора и интерпретатора языка Рефал-5:
>git grep -e "for\s*(" | bash -c "wc -l"
235
>git grep -e "while\s*(" | bash -c "wc -l"
151
Здесь использовал более хитрый шаблон поиска, поскольку предлог for часто 
используется в комментариях, написанных по-английски. В других проектах такой 
проблемы нет.

Цикл while применяется реже, но не «очень редко», 38 % в одном проекте, 26 % в 
другом, в третьем вообще нет, в четвёртом 39 %. Видно, что зависит от стиля 
программирования, но используется, по-моему, достаточно часто. Для других 
языков программирования статистика может быть и иной.

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

> Также принципиальным является отличие функции, которая является значением, от 
> функции с закрепленным в ее теле ее же именем.  Тело первой функции можно 
> подставить везде, где требуется ее значение (для вызова или другой цели) — 
> отсюда, в частности, вытекает удобство механических манипуляций с программой 
> — а со второй так сделать невозможно.  Уместно ли обязательно ставить функцию 
> во второй, ограниченный класс, только потому что она рекурсивная?
Возникает естественная аналогия со структурным программированием (и 
неструктурным тоже). Если циклы оформлять через «метка+goto», то, если метки 
глобальны, механически перенести такой цикл в другое место затруднительно 
(потребуется переименовать метку). Решений у проблемы несколько:
* Создать области видимости для меток. Например, ограничить область видимости 
подпрограммой или файлом.
* Ввести «локальные» метки. Такая метка может повторяться в программе несколько 
раз, но goto на неё связывается с ближайшей по тексту. Такое практикуется в 
синтаксисе некоторых ассемблеров.
* Ввести конструкцию, не требующую именованной метки: структурный цикл, 
подразумевающий в конце переход в начало, замаскированные goto, связываемые с 
ближайшим окружающим циклом — break и continue. К слову, развитием этой мысли 
являются break и continue с метками, например, в Java.
Фактически, вы предлагаете конструкцию, аналогичную continue, но для рекурсии 
(аналог break уже есть, называется return).

> В плане практическом, возьмем такую ситуацию.  Часто бывает так, что вызов 
> рекурсивной функции ее пользователем отличается от самовызова — например, 
> из-за того, что при первом вызове нужно что-то сделать один
раз: как правило, установить какие-то начальные значения.  Но чаще всего, 
вызываемой функции нечем отличить первый вызов от самовызовов, а даже если это 
возможно, то невыгодно проверять каждый раз.
> Это противоречие устраняется вложением в основную функцию «вспомогательной», 
> которая на самом деле выполняет работу по существу и, в частности, 
> самовызывается рекурсивно.  «Основная» же функция только подготавливает 
> начальные параметры к ее работе и, возможно, как-то изменяет результат до 
> того как передать его пользователю.
До боли знакомая ситуация. Я много программировал на Базисном Рефале, где из 
управляющих конструкций есть только функции, и эти функции глобальны. Функция 
служит и ветвлением (выбор осуществляется сопоставлением с одним из образцов), 
и циклом (рекурсия). И обычной ситуацией является запись какой-то повторяющейся 
операции в виде двух функций. Одна служит «фасадом» — принимает значимые 
аргументы, вторая — циклом. «Фасад» подготавливает для «цикла» дополнительные 
переменные-аккумуляторы, которые хранят своё значение между итерациями.
Если бы я программировал на Рефале с именованными вложенными функциями, и там 
бы мне потребовался бы цикл, то я написал бы: $func Loop { …… }. А внутри 
функции вызывал бы <Loop …>. Для дважды вложенных функций использовал бы имена 
вроде Loop-ЧтоТоОдно, Loop-ЧтоТоДругое. Сейчас подумываю о соответствующих 
ключевых словах, например <$repeat …>, <$repeat0 …> (синоним $repeat), 
<$repeat1 …> (для окружающей)… Либо, <$loop …>, <$rec …> или какое-нибудь иное 
слово. Вообще, такое ключевое слово можно использовать и в обычных глобальных 
именованных функциях для рекурсивного вызова. Тоже смотрелось бы неплохо.

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

> …а вложенную функцию можно даже скопировать в другое подходящее окружение, не 
> изменяя ничего в ней, в частности — интерфейс к «родителю».
Копипаст — зло. Если нужно скопировать код, чтобы повторить одинаковую логику, 
то эту логику нужно вынести в отдельную функцию, которую следует вызывать из 
двух мест. А функция будет именованной :-). Впрочем, если язык поддерживает 
макросы, то можно такой фрагмент кода использовать внутри макроса. Пишут же на 
Си макросы, содержащие, скажем, break, continue или return.

-----Original Message-----
From: Boyko Bantchev [mailto:boyk...@gmail.com] 
Sent: Saturday, August 5, 2017 10:27 PM
To: refal@botik.ru
Subject: Re: FW: Рефал умер?

Здравствуйте, Александр,

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

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

Вот, например, в истинности утверждения

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

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

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

(Впрочем, по поводу утверждения «нужна редко», вспомнил о цикле со 
след-условием (do-while или repeat-until).  Он часто используется?
Судя по моей практике, крайне редко.  Тем не менее, он есть в почти всех 
императивных языках.  А зачем нужен while в языке C и подобных, когда он — 
всего лишь частный случай for, с тяжелыми ограничениями?
Насколько часто нужно написать цикл без ни вводной, ни обновляющей частей?  
По-моему, очень редко.  Но и эта конструкция присутствует практически везде в 
императивных языках.)

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

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

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

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

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

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

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

Ответить