Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

Циклы

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

  • Универсальный:
    for (int i=0; i < lenght; i++)
    {	// тело цикла
    }
    
  • Универсальный в BASIC-стиле:
    FOR I=1 TO LENGHT STEP 1
       // тело цикла
    NEXT
    
  • Цикл «пока»:
    while (условие)
    {	// тело цикла
    }
    
  • Цикл с проверкой в конце:
    do
    {	// тело цикла
    } while (условие)
    
  • Цикл «for each»:
    for each (некое множество)
    {	// тело цикла
    }
    
        С одной стороны, программисты хотят иметь удобные циклы на все случаи жизни, что поощряет их разнообразие в языках программирования. С другой стороны, существует универсальный оператор цикла, который может заменить собой многие из этих циклов. Он же универсален! Интересный взляд на циклы можно обнаружить в статье Андрея Андреева.

        Надо заметить, что мысли о большей роли универсального цикла имеет под собой прочные и обосноснованные аргументы. Именно поэтому не стоило перечислять все виды циклов в начале статьи. Мы должны придумать такой оператор цикла, который действительно позволит заменить собой все остальные. Можно заметить, что универсальный оператор цикла C/C++ имеет недостаток. С его помощью нельзя проверить условие в конце цикла. Это проблему надо решить.

        Для начала рассмотрим подробнее устройство цикла в C/C++:
универсальный цикл в C и C++
        Действия, выполняемые в цикле, графически можно изобразить так:
Схема действий в цикле с проверкой вверху


        «Допиливаем» этот оператор, за основу берём придуманный нами «симметричный скобочный» стиль. Си-шный стиль меняем на свой:
(for int i=0; i < lenght; i++
	// тело цикла         )
        А потом пристально его рассмотрим. В универсальном цикле после ключего слова идут выражения, инициализирующие цикл. Количество этих выражений может быть равным нулю, единице и больше. Далее — условие продолжения цикла. Количество условий — ноль или единица. Далее — выражения, выполняющиеся в конце цикла. Количество этих выражений — нуль, единица и больше.

        Первым делом в этом цикле хочется заменить ключевое слово «for» («для»). Для программ, которые пишутся на русском языке, самое уместное ключевое слово — это слово «цикл». Просто называем вещи своими именами. Что наиболее подходит для английского языка? В языке Ada используют ключевое слово «loop» («петля»). Не знаю, почему там не прижилось «cycle».

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

        Ну и третий момент — заменяет «i++» на «++i» по причинам, которые объясняются в другой статье. В итоге получаем:
(int i=0 loop i < lenght; ++i
	// тело цикла         )
        Или в более общем виде:
(инициализирующие выражения loop условие; выражения в конце цикла
	// тело цикла         )
        Изобразим это графически:
цикл с проверкой вверху
        Суть этого цикла та же, что и в Си. Отличаются они лишь синтаксисом, внешним видом, семантика на 100% та же. Наша гипотетическая среда разработки могла бы отобразить это так:

(       int i=0 loop  i < L;  ++i  
  // ...  
 
(       int j=0  loop  j < M; ++j  
  // ...  
 
(       int k=0  loop  k < N; ++k  
  // ... )

            А как должен выглядеть цикл «пока»? От универсального цикла его отличает отсутствие инициализирующих выражений и выражений, которые выполняются в конце цикла. В C/C++ такое «обрезание» универсального цикла выглядит так:
for (;условие;)
            Этой кошке можно отрезать хвост по частям до полного его исчезновения:
for (инициализирующ.выр-я; условие; выр-я в конце цикла) // исходный вид
for (; условие; выражения в конце цикла)                 // нет инициализации
for (инициализирующ.выр-я;; выр-я в конце цикла)         // нет условия
for (инициализирующ.выр-я; условие;)                     // нет выр-й в конце
for (инициализирующ.выр-я;;)                             // только инициализация
for (; условие;)                                         // подобен while(условие)
for (;; выр-я в конце цикла)                             // только выр-я в конце
for (;;)                                                 // вообще пусто
        А теперь то же, но в «симметричном скобочном» стиле:

(       инициализирующ.выр-я loop условие; выр-я в конце цикла  
  // ...  
 
(       loop условие; выр-я в конце цикла  
  // ...  
 
(       инициализирующ.выр-я loop ; выр-я в конце цикла  
  // ...
(       инициализирующ.выр-я loop условие;  
  // ... )
)

(       инициализирующ.выр-я loop ;  
  // ...  
 
(       loop условие;  
  // ...  
 
(       loop ; выр-я в конце цикла  
  // ...
(       loop ;  
  // ... )
)

            Гибкость — не меньшая, чем в C/C++, но при большей лаконичности. Это гибкость — ещё и в том, что мы собираемся применять этот цикл вместо того, что называют «циклом с проверкой в конце». Чем отличается цикл «do {тело цикла} while (условие)» от цикла «while (условие) {тело цикла}»? Да тем, что во втором случае решение о продолжении цикла принимается в конце цикла, а впервом случае — в начале. Об этом нам недвусмысленно говорит фраза «while (условие)»: если она стоит вначале, то и проверка вначале. Если в конце, то и проверка в конце.

            Универсальный же цикл «for (выражения инициализации; условие; выражения в конце цикла» не похож на циклы «while» и «do while» тем, что содержит выражения, нарушающие порядок «слева направо, сверху вниз». Если считать этот вид цикла некой функций с тремя операндами, то третий операнд (выражения в конце цикла) выбивается из общего ряда. Эта особенность привычна программистам и ни у кого не вызывает отторжения.

            Наша идея состоит в том, чтобы переместив строку с «операндами» цикла в конец, заставить его работать как цикл «do {тело цикла} while (условие)»:
(  // тело цикла
   инициализирующ.выр-я loop условие; выр-я в конце цикла)
            Графически это можно изобразить так:
цикл с проверкой вверху
            Ниже — действия, которые в этом цикле выполняются:
Схема действий в цикле с проверкой внизу
            Непривычно? Только на первый взгляд. Это всё тот же универсальный цикл, вот только условие теперь проверяется не в начале, а в конце. А вот первый «операнд» (инициализирующие выражения) выполняется единожды перед тела выполнением цикла — как обычно. И третий операнд выполняется как обычно в конце. Так что такие циклы не вызовут аллергии у программистов. А среда разработки поможет своей графикой разобраться, где мухи, а где котлеты:

( // тело цикла
(    
  // ...  
 
( // ...
( // ...  
      loop ; )
 
  // ...  
      loop ; выр-я в конце цикла )
 
  // ...  
        инициализирующ.выр-я loop условие; выр-я в конце цикла )
 
  // ...  
        инициализирующ.выр-я loop условие; выр-я в конце цикла )


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

( // тело цикла
(    
  // вложенный цикл с проверкой условия в средине  
   
        инициализирующ.выр-я loop условие; выр-я в конце цикла  
  // продолжение тела вложенного цикла )
 
  // ...  
        инициализирующ.выр-я loop условие; выр-я в конце цикла  
  // ...  
  // продолжение тела цикла )


            Зачем это надо? Так ведь нередки циклы, где проверка условия выхода проверяется где-то внутри цикла. Допусти, так:
for (;;)
{     // тело цикла
     if (условие)
        break;
     // продолжение тела цикла
}
            Мы же, перемещая строку с описанием цикла в средину, позволить сэкономить на операторе «if». Так что нами описан не только рабочий, но и интересный вариант. Выгоды от применения от такого цикла очевидна: его параметры записываются унифицированным способом. Меняется только место проверки условия, которое совпадает с положением управляющей строки цикла. А какие проблемы могут от этого возникнуть?
  • Непривычная для программистов вид цикла, когда он употреблён в конце или средине.
  • Усложнение компилятора. Ему надо будет научиться находить ключевое слово «loop» в самых разных местах цикла. Компилятор должен будет «заглядывать» далеко вперёд в поисках «loop»:
    (
        // куча операторов
        // ..
       ... loop  ... )
    
            Всё ли описано? Нет, ещё предстоит рассмотреть цикл «для каждого» («for each»), а так же операторы «продолжение цикла» и «выход из цикла».

Цикл «для каждого»

        Этот вид циклов описывает циклически выполняющиеся действия для каждого элемента некого множества. Почему этот цикл отсутствует в C/C++? На это есть несколько причин. Они касаются особенно C, потому что в C++ появились некоторые «костыли», которые позволяют в некоторых случаях заиметь в своём распоряжении «for each».
  • Во-первых, в C нет такой абстракции, как «множество». В языке нет возможности передать некий объект, не заботясь о его сущности. Массив ли это, список, стек или дерево — как язык поможет определить конкретный вид множества, если, к примеру, указатель на него был передан в функцию?
  • Во-вторых, как определить количество элементов в этом множестве?
  • В-третьих, каким образом перебрать все элементы этого множества?
            От этих вопросов нельзя отмахнуться, как от несущественных. Но они связаны м понятием множества, которые мы ещё не рассмотрели. Поэтому этот вид цикла рассмотрим позже, в статье о цикле «для каждого» («for each»).

            P.S. Ответ на комментарий Михаила. Позвольте процитировать Википедию: «Цикл Дейкстры удобен при реализации некоторых специфических повторяющихся вычислений». Конструкции, которыми можно применять во всех ситуациях или их большинстве предпочтительнее тех, которые решают специфичные задачи. Особенно если первые просты и наглядны. Цикл
for (инициализация; проверка условия в начале; действия в конце цикла)
{ тело цикла}
легко меняется на цикл
(инициализация loop проверка условия в начале; действия в конце цикла
  тело цикла)
Цикл
while do (проверка условия в начале)
{ тело цикла}
соответственно меняется на цикл
(loop проверка условия в начале
  тело цикла)
Цикл
repeat until (проверка условия в конце)
{ тело цикла}
соответственно меняется на цикл
( тело цикла
loop проверка условия в конце)
В итоге имеем цикл, который, будучи универсальным, не становится громоздким. Всё просто и элегантно. При этом циклы могут досрочно прерываться, для этого имеются конструкции, которые перекрывают все потребности.

            «Цикл-паук» всё же очень громоздок. Некрасиво. Кажется неслучайным, что он не воплощён в каком-либо языке программирования.

Что ещё почитать на эту тему

Опубликовано: 2012.09.25, последняя правка: 2015.01.23    03:31

ОценитеОценки посетителей
   ████████████████████ 8 (47.0%)
   ████████ 3 (17.6%)
   █████████████ 5 (29.4%)
   ███ 1 (5.88%)

Отзывы

     2013/04/14 10:25, Михаил          # 

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

циклы с одной точкой выхода
for (фиксированное число повторений)
while do (цикл с предусловием)
repeat until (цикл с постусловием)

циклы с несколькими точками выхода
loop ... exit ... exit ... end; (бесконечный цикл + оператор его прерывания)
цикл паук, цикл дейкстры и пр.

     2013/04/14 13:29, Автор сайта          # 

Ответ читайте выше.

     2013/04/18 20:13, Михаил          # 

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

     2013/04/19 13:45, Автор сайта          # 

Вопрос и ответ перенес в ветку по теме: Синтаксический сахар

     2013/04/18 20:22, Михаил          # 

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

     2013/04/19 14:07, Автор сайта          # 

Повторили решение Си авторы C++, C#, Java, Perl, PHP, D, Cyclone, Rust и многих других. Я же творчески переработал. Переместил ключевое слово внутри «параметров» цикла, предложил использовать его не только в начале цикла, но и в конце и даже в средине. Т.е. это ещё большая универсализация.

Цикл с фиксированным числом повторений есть:
( i=0 loop i < 100; ++i 
тело цикла)
Этот цикл выполнится фиксированное число раз, а именно 100.

Отдельная конструкция с прерываемым циклом не нужна. Если универсальный цикл записать так:
(loop
    (if a>b
          exit)
тело цикла)
То это выглядит так, как Вы этого хотите. Желаемая цель достигается.

А зачем бороться с «break» и «continue»? Ведь это не «goto»! Более того, отказ от «break» как провоцирует употребление «goto».

     2013/04/23 11:45, Михаил          # 

Пусть так, но зачем повторять не очень удачное решение примененное в Си? Вы ведь не хотите повторять составной оператор, которой применен в Си, С++, С# и т.д.

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

Желаемой цели (реализации алгоритма) можно достичь с помощью всего двух операторов: if и goto. Я же о семантике. Ваш вариант не обеспечивает фиксированной семантики для конструкции, что затрудняет её понимание.

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

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

     2013/04/25 11:58, Автор сайта          # 

«break» и «continue» помогают исключить из языка «goto», вред которого теоретически доказан. Не могли бы Вы дать ссылку на авторитетные источники, где обоснован вред «continue»?

С помощью «if» можно обойтись без «continue». Но заменить «continue N» сложнее, появятся лишние операторы.

«break» не предлагается к использованию в других местах, кроме циклов. В операторе «switch» его предлагается исключить, т.к. он провоцирует появление «спагетти-кода».

В Паскале цикл с фиксированным числом повторений выглядит так:
for V:= E1 to E2 do S, где E1 и E2 — выражения, которые могут иметь побочные эффекты.

Чтобы избежать побочного эффекта при каждой итерации, нужно просто E2 вычислить однократно до начала цикла. В C++ это можно сделать так:
for (int i=E1, int N=E2; i<=N; ++i) S;.
Если E2 — константа или переменная, то побочных эффектов не будет. Но это частный случай. Но стоит ли делать 2 цикла,
for V:= CV1 to CV2 do S, где CV1 и CV2 — константа или переменная, и 
for V:= E1 to E2 do S, где E1 и E2 — выражения?
Вам известны языки программирования, грамматика которых запрещала бы использование выражений для задания числа повторенний?

     2014/01/16 11:20, Pensulo          # 

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

Тогда конструкция циклического исполнения программного блока могла бы выглядеть, например, следующим образом:
<инициализация_цикла>
(loop
if пред_условие leave
<часть_тела_цикла_1>
if сред_условие levae
<часть_тела_цикла_2>
if пост_условие leave
)
Где leave по-сути тот же самый exit, что и в ваших примерах, но тогда сохраняется возможность осуществлять досрочный выход из функции с помощью служебного слова exit.

Размещать анализаторы выхода if .. leave можно в любом месте внутри циклических скобок (loop .. ) и в любом количестве.

А вот с предоставлением возможности выхода из объемлющего цикла, морально ОЧЕНЬ трудно согласиться.

     2014/01/16 12:15, Автор сайта          # 

А зачем инициализатор выносить за скобки цикла? В том то вся выгода, что вся конструкция цикла (впрочем — любая конструкция) находится внутри скобок. Мы что инициализируем, переменные цикла? Значит это надо сделать внутри конструкции цикла. Но это не значит, что до открывающей скобки ничего нельзя делать. Просто находящийся внутри скобок инициализатор подчеркивает, что он относится именно к циклу.

А какая альтернатива выходу из объемлющего цикла? Только «goto». Поэтому «exit 2» можно читать как «goto label2». Только первый способ короче.

     2014/04/23 02:40, 85.192.188.252          # 

Тогда конструкция циклического исполнения программного блока могла бы выглядеть, например, следующим образом:
<инициализация_цикла>
(loop
if пред_условие leave
<часть_тела_цикла_1>
if сред_условие leave
<часть_тела_цикла_2>
if пост_условие leave
)
Такой цикл уже придуман в Scheme (диалект Lisp'а). Аналогично там есть сложный составной условный оператор.

     2015/05/27 08:25, Алексей Яковлев          # 

А не будет loop теряться в коде, если он в середине или в конце? И ещё, как отделить инициализацию от тела цикла, если loop опять же в середине или конце?

     2015/05/27 15:15, Автор сайта          # 

IDE должна выделять ключевые слова: его символы должны быть жирными, иметь другой цвет. Можно поставить перед «loop» пиктрограмму. Блок, из которого состоит цикл, можно обрамлять специальной рамкой. Много способов обратить внимание на то, что это цикл.

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

     2016/03/25 06:43, rst256          # 

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

«640 килобайт хватит всем» http://lurkmore.to/640_килобайт
Конструкции, которые априори перекрывают все потребности, — это if-goto, но его давно репрессировали (кстати, почему он тогда не пропадает, а наоборот возникает там, где его раньше небыло? Например в lua(с v.5.3), php(хотя пеха ладно,это не язык)).
if-goto[1] — самый гибкий, у него 3 степени свободы (условие, позиция в коде, адрес перехода);
циклы while/repeat - 1 (условие);
с проверкой "в середине" (при условии что середина — это также начало с концом) - 2 (условие, позиция)

Последний вариант и правда "самый"[1] гибкий, хороший цикл с итераторами годен к употреблению.

     2016/08/22 17:33, xDDGx          # 

Идея, конечно, интересная, но синтаксис ужасен:
(int i=0 loop i < lenght; ++i
...
)
Чем это хуже традиционного for?
for(int i; i<length; ++i){
...
}
Во-первых, ключевое слово for стоит первым — оно сразу бросается в глаза, мы сразу понимаем, что перед нами цикл, а не что-то другое. То же касается while и do-while (ключевое слово do используется только в этой конструкции, так что мы сразу понимаем, что перед нами цикл do-while, а не что-то другое). В вашем варианте всё смешалось — слово loop может стоять в середине строки, а то и вовсе в конце всего блока или даже где-то в его середине.

Аргумент "с этим поможет ИДЕ" на данном этапе несерьёзен. Обычно сперва появляется язык (компилятор — а это уже громадный объём работы), затем — средства разработки, а не наоборот. К тому же, далеко не все и не всегда используют продвинутые ИДЕ, и, в принципе, программа, написанная на ЯП, близком к идеалу, должна быть читаемой и понятной в самом примитивном виде — в виде простого текста. Текущий синтаксис loop вредит читаемости.

Во-вторых, традиционный цикл for хорош тем, что мы сразу видим, где находятся параметры цикла, а где — его тело, нет неоднозначности. Всё единообразно — каждый параметр отделяется от другого точкой с запятой, выражения в списках — запятой. Вы сами приводили примеры в стиле "Мама зпт я хочу есть воскл Мы скоро пойдём домой вопр", доказывая необходимость "знаков препинания" в тексте программы, и сами же нарушили этот принцип. В (int i=0 loop i < lenght; ++i) мы видим, что после инициализирующего выражения (а если их несколько? как они отделяются друг от друга?) через пробел идёт ключевое слово loop, затем через пробел условие, а затем, через точку с запятой — действия конца итерации (и опять-таки, а если нам нужно в этом месте несколько выражений?).
Как мы (или даже компилятор), в данной конструкции можем однозначно понять, что является выражением (-ями) инициализации, что — условием, что — действиями в конце?
(
int i=0 loop i<length
++i
...
)
Правильно ли я понимаю, что такая конструкция будет аналогична самой первой? А такая?
(
int i=0
loop i<length
++i
...
)
А это — будет ли правильным циклом с двумя телами?
(
...
int i=0
loop i<length
++i
...
)
Как компилятор или человек разберётся, где начинаются (заканчиваются) блоки инициализации, условия и т. д.? В то же время, C/C++ позволяет нам написать, например, такое:
for(
int a=1,
b=0;
a<argc;
++a,
b+=2
){
...
}
Это может пригодиться в случае больших списков или сложных условий для улучшения читаемости.

Повторюсь: идея интересная, но я сомневаюсь, что многим понравится такой синтаксис. И я не думаю, что это принципиально, один у нас цикл или их несколько. Типовые сценарии использования всё равно придётся запоминать. Со своей стороны могу предложить такой синтаксис. Прямой аналог цикла for:
(loop (int i=0) i<length (++i)
...
)
Скобки обязательны. Если какие-то поля не нужны, просто оставляем их пустыми:
(loop () ()
...
)
Аналог цикла с двумя телами:
(do
...
loop (int i=0) i<length (++i)
...
)
Если второе тело (после строки с loop) не нужно, просто оставляем его пустым (получится продвинутый аналог цикла do-while, "do-for" так сказать. Мои варианты, конечно, тоже далеки от идеала, особенно последний, но, по крайней мере, мы будем уверены, что раз есть do — где-то должен быть loop.

     2018/05/24 01:25, Александр Коновалов aka Маздайщик          # 

Могу заметить только одно. Такой синтаксис описания цикла будет очень сложно парсить. В языке Си мы по первому ключевому слову if/for/while/do знаем, что за конструкция будет дальше и как её интерпретировать. То же самое с Паскалем. А вот с Вашим циклом гораздо сложнее.

Это оператор цикла
(f(x, j)
int j = 0; loop j < N; ++j
g(x, j))
А это просто выражение в скобках
(f(x, j))
Начав читать код, мы ещё не знаем, что будет дальше. Кстати, интересным представляется цикл Алгола-68:
while E1 do E2 od
Интересно тут то, что и E1, и E2 могут быть последовательностью операторов. При этом в E1 проверяется значение последнего выражения. Цикл с предусловием:
j := 0; while j < 10 do print(j); j +:= 1 od
Цикл с постусловием (между do-od пустой оператор):
j := 0; while print(j); j +:= 1; j < 10) do ; od
Цикл с выходом посередине
j := 0; while print(j); j < 10 do print(2*j); j +:= 1 od
Процедура построения пирамиды:
proc sift = (ref [> T a, int n, int p) void: (
int j;
T x := a[p>;
while (
if (j := 2*p + 1; j + 1 < n) then if a[j> < a[j + 1> then j +:= 1;
if j < n then a[j> > x else false
) do
a[p> := a[j>; p := j
od;
a[p> := x
)
(and здесь не ленивый, поэтому вместо «j < n and a[j> > x» записан if).

     2018/05/24 14:09, Автор сайта          # 

Цикл «while E1 do E2 od» плох тем, что он по идее должен закончиться «elihw», а заканчивается «od». Но и «elihw» — тоже плохо, это нечитаемая абракадабра. Поэтому лучше «(loop . . . )» или «(. . . loop)». Если насчёт последнего вы считаете, что

Начав читать код, мы ещё не знаем, что будет дальше.

то, во-первых, код нельзя писать длинным, чтобы «loop)» не пряталось за горизонтом. Во-вторых, должна помогать синтаксическая раскраска блоков в IDE.

     2018/05/25 00:48, Александр Коновалов aka Маздайщик          # 

Цикл „while E1 do E2 od“ плох тем, что он по идее должен закончиться „elihw“, а заканчивается „od“.»

Вообще, в Алголе-68 «ядро» цикла выглядит как do … od. А целиком цикл выглядит примерно так:
for V from A to B step C while D do … od
Здесь for, from, to, step, while — «префиксы». И любой из них может отсутствовать.

Если присутствуют все, то переменная V принимает последовательно значения от A до B, цикл завершается либо после достижения V > B, либо когда D станет false.

Если присутствуют for, from и to, но нет while, то это обычный цикл со счётчиком, известный по Бейсику и Паскалю.

Если step отсутствует, шаг подразумевается равным 1. Если отсутствует from, подразумевается нижняя граница тоже равная 1. Может также отсутствовать «for V» — будет происходить итерация по «невидимой» переменной:

to 100 do … od — цикл выполнится 10 раз.

while D do … od — цикл без счётчика с одним условием.

do … od — «бесконечный» цикл. Выход возможен только через goto (как мне помнится, break и return в Алголе-68 не было).

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

Если насчёт последнего вы считаете, что „Начав читать код, мы ещё не знаем, что будет дальше.“ ... во-первых, код нельзя писать длинным, чтобы „loop)“ не пряталось за горизонтом. Во-вторых, должна помогать синтаксическая раскраска блоков в IDE.

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

Я говорил о сложности синтаксического анализа. Говоря «мы» в своём комментарии («Начав читать код, мы ещё не знаем…») я ставил себя на место парсера (в компиляторе или в IDE), которому, читая текст по символу от начала до конца, трудно понять, что у нас в скобках — выражение, последовательность выражений или цикл. Разбор таких конструкций усложняет парсер и грамматику.

Вы и сами об этом пишете:

Усложнение компилятора. Ему надо будет научиться находить ключевое слово „loop“ в самых разных местах цикла. Компилятор должен будет „заглядывать“ далеко вперёд в поисках „loop“».

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

Можно такой код разбирать методом «перенос-свёртка», он справится. Только для «переноса-свёртки» нужно генерировать таблицы сторонними инструментальными средствами, вроде YACC.

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

P.S. Ссылка «в статье о цикле «для каждого» („for each“).», http://www.compiler.su/tsikl-dlya-kazhdogo.php, не рабочая. Или нужно ссылку убрать, или статью написать.

     2018/05/26 00:26, Автор сайта          # 

Когда я писал «должен закончиться elihw», я имел в виду не Алгол-68, а гипотетический язык, который близок к идеалу в синтаксисе. А как выглядят циклы в Алгол-68 — в этом нет никакой тайны. В них не соблюдается принцип симметрии по типу «if» — «fi».

Проблемы с поиском выражений типа «loop)» решаются на этапе лексического анализа. Правда, для этого лексический анализ выделяется в отдельный проход. Код, решающий эту проблему, — на несколько строк... Там несложный алгоритм и я даже описал бы его, но... Некоторые идеи, изложенные на этом сайте, потом нахожу в недавно появившихся языках. Се ля ви.

Цикл «для каждого» будет описан. Но как скоро — не знаю.

     2018/05/27 16:38, Александр Коновалов aka Маздайщик          # 

Проблемы с поиском выражений типа „loop)“ решаются на этапе лексического анализа. Правда, для этого лексический анализ выделяется в отдельный проход. Код, решающий эту проблему, — на несколько строк... Там несложный алгоритм и я даже описал бы его, но...

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

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

Интересно было бы посмотреть примеры.

     2018/05/27 22:56, Автор сайта          # 

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

Не удивительно, что разные люди имеют разные мнения. Понимаете, скобки, являясь одной из основных конструкций в языке и при гарантии одинакового количества открывающих и закрывающих скобок, позволяют отследить их парность. Как только появилась n-ая «(», мы переходим на n-ый уровень вложенности скобок. Помечаем, что скобка — отдельная. Если затем встретилось ключевое слово «if», то вносим исправление — это скобка не одиночная, это на самом деле «(if». То же самое с «loop». Встретили это слово — помечаем, что n-ая открывающая скобка — это цикл с заголовком посредине. Это можно сделать в лексере.

Интересно было бы посмотреть примеры.

Ну тогда пишите мне на почту, выскажу Вам тет-а-тет.

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

функциональный язык программирования ... поскольку он функциональный, в нём циклов нет. Вместо них используется рекурсия.

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

     2018/05/28 00:10, Александр Коновалов aka Маздайщик          # 

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

Как можно циклы заменить на рекурсию. Пример будет для наглядности на Си:
// итеративный факториал:
int fact(int n) {
int i = 1;
int prod = 1;
while (i <= n) {
prod = prod * i;
i = i + 1;
}
return prod;
}

// рекурсивный факториал:
int fact(int n) {
return do_fact(1, 1, n);
}

int do_fact(int i, int prod, int n) {
if (i <= n) {
return do_fact(i + 1, prod * i, n);
} else {
return prod;
}
}
Общее правило: цикл while превращается в рекурсивную функцию, переменные цикла — в параметры этой функции. Вместо присваивания переменным нового значения вызываем функцию-цикл с новыми параметрами.

Понятно, что return do_fact(…) должен накапливать фреймы стека, но для функциональных языков есть такая штука, как оптимизация хвостовой рекурсии. Если выражение в return представляет собой рекурсивный вызов, то компилятор присваивает параметрам новые значения и просто делает jump на начало функции. Получается цикл. Автор сайта наверняка знаком с оптимизацией хвостовой рекурсии, я пишу другим читателям сайта.

К примеру, необходимо в цикле проверять состояние чего-либо. Как с этим справится рекурсия?

Какое именно состояние надо проверять? Если речь идёт о переменных цикла — вот они, в параметрах. Если проверяется существенно глобальное состояние, например, наличие файла, то оно просто проверяется.
void wait_for_existing(string filename) {
if (exist_file(filename)) {
wait_for_existing(filename);
}
}

     2018/05/29 21:48, Автор сайта          # 

У программиста-функциональщика, чье сознание не замутнено знанием технических деталей, рука не дрогнет написать бесконечную рекурсию:
(функция проверить состояние и обработать его (предыдущее состояние)
текущее состояние = проверить (...)
(если текущее состояние != предыдущее состояние
обработать (текущее состояние))
проверить состояние и обработать его (текущее состояние)) // хвостовая рекурсия
Программист, который «в курсе», особенно системный, схватится за голову: каждый последующий вызов пополняет стек адресом возврата (как минимум). Без оптимизации (замена рекурсии циклом) это неработоспособно. Обычно к оптимизации прибегают на конечных этапах разработки («Преждевременная оптимизация — корень всех зол»), а тут надо будет менять привычки.

Но надо отдать должное: такой код снижает количество ветвлений на языке разработки. Код становится как бы «плоским». Мимо такого приёма — замены циклов рекурсией — просто нельзя пройти.

     2018/05/31 17:39, Comdiv          # 

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

     2018/05/31 17:57, rst256          # 

Не удивительно, что разные люди имеют разные мнения. Понимаете, скобки, являясь одной из основных конструкций в языке и при гарантии одинакового количества открывающих и закрывающих скобок, позволяют отследить их парность. Как только появилась n-ая «(», мы переходим на n-ый уровень вложенности скобок. Помечаем, что скобка — отдельная. Если затем встретилось ключевое слово «if», то вносим исправление — это скобка не одиночная, это на самом деле «(if». То же самое с «loop». Встретили это слово — помечаем, что n-ая открывающая скобка — это цикл с заголовком посредине. Это можно сделать в лексере.

Ну так то да, это можно сделать в лексере. Можно, но не нужно. Парсер будет отслеживать их парность гораздо лучше, чем лексер, и пометки сделанные лексером просто бесполезны. Лексер, в отличии от парсера, не разбирает конструкции языка. Если мы пропустим ")" после вызова функции и после этого уберем "(" открывающую тело цикла loop, то лексер тупо пометит скобку "(" в вызове функции как начало цикла, после чего нам остается только молить бога что бы эти пометки нигде далее в компиляторе не использовались.

Примерный алгоритм разбора парсером (с построением АСТ).
Если появилась скобка в режиме разбора предложений(statement), вызываем рекурсивно разбор блока кода.
В функции разбора блока:
в цикле разбираем отдельные команды, пока не появится ")"
если получаем команду "if", тогда
если список команд в блоке пуст, тогда
преобразовываем блок в констр. ветвления и переходим к её разбору
иначе
ошибка "неожиданный if"
если получаем команду "loop" тогда
если тип блока == простой блок тогда
меняем тип блока на loop
иначе
если тип блока != loop тогда
ошибка "loop в блоке "+тип блока
иначе
ошибка "в блоке loop уже задано условие"
// и т.д.

     2019/09/05 15:06, kt          # 

Очень понравилась идея цикла по «невидимой» переменной из Алгола-68, о которой упомянул Александр Коновалов aka Маздайщик, спасибо ему. Вставил сегодня такую конструкцию в свой транслятор:
DO TO 100; ... END;
Или «по-русски»
ЦИКЛ ДО 100; ... КОНЕЦ;
Тут дело вот в чём: у нас несколько раз в текстах встречаются циклы-итерации, где переменная не является никаким индексом в теле цикла. И вот, забыв об этом, ищешь, ищешь какую-нибудь I из заголовка. Да где же она, чёрт возьми, используется? Оказывается нигде!

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

P.S. Жаль, что в своё время по Алголу-68 выпустили «не ту» книгу. Все ожидали инструкцию пользователю, а вышли рабочие материалы для разработчиков компилятора, по которым учиться этому языку было совершенно невозможно. К Алголу-68 я бы сейчас вообще присмотрелся бы заново: там много всяких языковых идей.

     2019/09/05 17:50, Автор сайта          # 

DO TO 100

Для полного счастья осталось добавить оператор «foreach», чтобы не заморачиваться числом элементов в массиве.

К Алголу-68 я бы сейчас вообще присмотрелся бы заново

Да, были разговоры, что этот язык опередил своё время. Но с другой стороны, почему из него не «таскают» идеи? Вроде бы язык не секретный. Литературы маловато — это да.

     2019/09/05 23:08, kt          # 

Нет уж. Хватит дуэлей! Хватит ключевых слов!
пусть остается
DO TO DIM(X) 

вместо
DO TO foreach(x)

     2019/09/12 20:40, Александр Коновалов aka Маздайщик          # 

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

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

Впрочем, это исторический опыт попытки формально описать не только синтаксис, но и семантику, а также сделать описание максимально компактным. Возможно, это опыт скорее «как не надо делать».

К Алголу-68 я бы сейчас вообще присмотрелся бы заново: там много всяких языковых идей.

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

Кстати, при переводе материалов по Алголу-68 на русский язык английское слово reference было переведено как имя. Соответственно неявное преобразование типа ref T, по-русски имя T, по смыслу — ссылка на T, к типу T называлось… разыменованием! А сейчас программисты говорят «разыменование указателя» и не задумываются, откуда взялось такое странное слово (если приглядеться, оно действительно странное).

     2021/03/27 13:37, Виталий Монастырский          # 

У меня всё предельно просто...
for i : 0
if i <= 15 (проверка цикла i стоит в начале блока цикла)
for j : 12
for n : 5, m : 0
n--
if n = m (проверка цикла n,m стоит в середине блока цикла)
m++
j--
if j >1 (проверка цикла j стоит в конце блока цикла)
i++
Проверяй, где хотишь...

     2021/04/01 15:08, Александр Коновалов aka Маздайщик          # 

А мне не просто. Обязателен ли инкремент/декремент в конце цикла? Может ли быть ни одной или более одной проверки?

     2021/04/02 13:57, Автор сайта          # 

У синтаксиса циклов Python есть известный недостаток. Не существует цикл с проверкой условия в конце, как это делается в Си:
do {
< тело цикла >
while(< условие >);
Ведь что такое блок в Python? Это строка с отступом n пробелов и идущие ниже строки с отступом, в котором пробелов больше n.
< --- n --- > первая строка блока
< --- n+m --- > идущие ниже строки блока
А что получается с циклом с проверкой внизу? Последняя строка блока должна иметь меньший отступ, чем предшествующие строки! Это вступает в противоречие с общим стилем языка. Можно, конечно, изобрести ключевое слово «loop_with_check_below» (цикл с проверкой внизу): сама надпись сделана вверху, а действует внизу
 loop_with_check_below < условие >
< тело цикла >
Ведь в том же Си-шном операторе «for» операторы приращения написаны вверху, а выполняются внизу:
for (< инициализация >;< условие >;< операторы приращения >) {
< тело цикла >
} // а здесь выполняются операторы приращения
Но автор Python решил обойтись без «loop_with_check_below».

     2021/04/02 19:32, Александр Коновалов aka Маздайщик          # 

Недостаток известен, лечится такой идиомой:
while True:

… тело цикла …

if 〈условие выхода〉:
break
Напомню читателям, что в Паскале цикл с постусловием оформлялся как
repeat

… тело цикла …

until 〈условие выхода〉;
В отличие от Паскаля, в Си в цикле с постусловием используется условие продолжения.

Добавить свой отзыв

Написать автору можно на электронную почту
mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Содержание

Каким должен быть язык программирования?

Анализ и критика

●  Устарел ли текст как форма представления программы

●  Русский язык и программирование

●  Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

●  Некоторые «вкусности» Алгол-68

●  «Двухмерный» синтаксис Python

●  Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

●  Должна ли программа быть удобочитаемой?

●  Стиль языка программирования

●  Тексто-графическое представление программы

●●  Разделители

●●  Строки программы

●●  Слева направо или справа налево?

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

●●  Длинные комментарии

●●  Короткие комментарии

●●  Комментарии автоматической генерации документации

●●  Нерабочий код

●●  Помеченные комментарии

●  Нужны ли беззнаковые целые?

●  Шестнадцатиричные и двоичные константы

●  Условные операторы

●  Переключатель

●  Циклы

●●  Продолжение цикла и выход из него

●  Некошерный «goto»

●  Изменение приоритетов операций

●  Операции присвоения и проверки на равенство. Возможно ли одинаковое обозначение?

●  Так ли нужны операции «&&», «||» и «^^»?

●  Постфиксные инкремент и декремент

●  Почему в PHP для конкатенации строк используется «.»?

●  Указатели и ссылки в C++

●●  О неправомерном доступе к памяти через указатели

●  Обработка ошибок

●  Функциональное программирование

●●  Нечистые действия в чистых функциях

●●  О чистоте и нечистоте функций и языков

●●  Макросы — это чистые функции, исполняемые во время компиляции

●●  Хаскелл, детище британских учёных

●●  Измеряем замедление при вызове функций высших порядков

●●  C vs Haskell: сравнение скорости на простом примере

●●  Уникальность имён функций: за и против

●●  Каррирование: для чего и как

●●  О тестах, доказывающих отсутствие ошибок

●  Надёжные программы из ненадёжных компонентов

●●  О многократном резервировании функций

●  Использование памяти

●  Почему динамическое распределение памяти — это плохо

●  Как обеспечить возврат функциями объектов переменной длины?

●●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●●  Массивы переменной длины в C/C++

●●  Размещение объектов в стеке, традиционный подход

●●  Размещение объектов переменной длины с использованием множества стеков

●●  Размещение объектов переменной длины с использованием двух стеков

●●  Реализация двухстековой модели размещения данных

●●  Двухстековая модель: тесты на скорость

●●  Изменение длины объекта в стеке во время исполнения

●●  Размещение объектов переменной длины с использованием одного стека

●  Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

●  Безопасность и размещение объектов переменной длины в стеке

●  Массивы, структуры, типы, классы переменной длины

●  О хранении данных в стеке, вместо заключения

●  Реализация параметрического полиморфизма

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Новости и прочее




Последние отзывы

2024/04/18 11:14 ••• Ivan
Энтузиасты-разработчики компиляторов и их проекты

2024/04/18 04:47 ••• void
Признаки устаревшего языка

2024/04/11 00:08 ••• Автор сайта
Постфиксные инкремент и декремент

2024/04/09 23:50 ••• Автор сайта
Русский язык и программирование

2024/04/07 15:33 ••• MihalNik
Все языки эквивалентны. Но некоторые из них эквивалентнее других

2024/04/01 23:39 ••• Бурановский дедушка
Новости и прочее

2024/04/01 23:32 ••• Бурановский дедушка
Русской операционной системой должна стать ReactOS

2024/03/22 20:41 ••• void
Раскрутка компилятора

2024/03/20 19:54 ••• kt
О многократном резервировании функций

2024/03/20 13:13 ••• Неслучайный читатель
Надёжные программы из ненадёжных компонентов

2024/03/07 14:16 ••• Неслучайный читатель
«Двухмерный» синтаксис Python

2024/03/03 16:49 ••• Автор сайта
О неправомерном доступе к памяти через указатели

2024/02/28 18:59 ••• Вежливый Лис
Про лебедей, раков и щук