Запахи кода и антипаттерны в Рефале

66 views
Skip to first unread message

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 11, 2020, 8:09:52 PM12/11/20
to re...@botik.ru

Доброй ночи всем!

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

 

Определения

Есть два близких, но разных понятия: запахи кода и антипаттерны.

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

Примеры запахов в ООП: дублирование кода, длинный класс, длинный метод, длинные списки параметров, параллельные иерархии наследования, мёртвый код…

Подробнее:

https://ru.wikipedia.org/wiki/Код_с_запашком
https://refactoring.guru/ru/refactoring/smells

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

Антипаттерн — это распространённый подход к решению класса часто встречающихся проблем, являющийся неэффективным, рискованным или непродуктивным (определение из Википедии). Т.е. распространённая, но плохая «идиома» программирования.

Примеры антипаттернов: программирование методом копипаста, магические константы, спагетти-код (слишком запутанная логика), лазанья-код (слишком много слоёв абстракции), зависимости по побочному эффекту, жёсткое кодирование (hard code) — зашивание в код того, что должно быть вынесено в конфигурацию, мягкое кодирование (soft code) — в конфигурацию вынесено слишком много, конфигурирование само по себе превращается в программирование.

Подробнее:

https://ru.wikipedia.org/wiki/Антипаттерн

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

 

Запахи и антипаттерны в Рефале

Из запахов кода в Рефале я могу перечислить только платформенно-независимые. Т.е. взяв уже упомянутый список с refactoring.guru и вычеркнув из него вещи, характерные для ООП:

·         Длинная функция. Менее актуально для Рефала-2, более актуально для других диалектов. В Рефале-2 функция может быть длинной или из-за обилия предложений, или из-за длины самих выражений в левой и правой части. В других диалектах Рефала функция может расти в длину из-за обилия управляющих конструкций, таких как условия, блоки, присваивания, действия… В Рефале-5λ вообще можно писать вложенные функции, например, внутри Map, и их тоже можно написать длинными.

·         Большой класс. В Рефале нет классов. Но можно сказать, что если файл с исходным текстом длинный, значит в нём намешано много всего и его, возможно, стоит разделить на несколько с разными подмножествами функций.

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

·         Группы данных. Можно передавать какие-то значения в виде отдельных кусков, а не сгруппировав их в некоторую абстракцию. Например, компилятор отслеживает номер строки, номер символа в строке и имя файла для указания точного места в сообщениях об ошибке. Можно их передавать как s.Line s.Col e.FileName, а можно передавать как t.Pos, внутри которого лежат искомые значения.

·         Расходящиеся модификации, стрельба дробью — при внесении изменений приходится делать правки в разных местах программы.

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

·         Дублирование кода — тут всё понятно.

·         Мёртвый код — в результате развития в программе появился код, который никогда не выполняется. Его надо удалить.

·         Теоретическая общность. Задача была решена с запасом на будущее, хотя решение для частного случая было бы проще и компактнее. Время показало, что этот запас оказался избыточен.

Остальные пункты относятся только к ООП.

Каких-то запахов кода, характерных именно для Рефала, я назвать не могу.

 

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

https://ru.wikipedia.org/wiki/Антипаттерн#Антипаттерны_в_кодировании

к Рефалу применима где-то половина списка. Поэтому приведу антипаттерны, характерные для Рефала:

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

·         Слишком общий образец. Образец одного или нескольких предложений более общий, чем документированный формат функции. Проблема в том, что такой образец может скрыть ошибку в программе. Функция может где-то вызываться с неправильным форматом аргумента. И если бы все образцы были бы уточнением формата, функция упала бы, выявив ошибку. Но более общий образец (в предельном случае даже e.X) может съесть неправильный аргумент и программа вместо падения может падать где-то в другом месте или вычислять неправильный результат. Удачи в отладке!

·         «Нумерование» вспомогательных функций. Т.е. имена вспомогательных функций для функции FuncName будут иметь вид FuncName1, FuncName3, FuncName4… Чтобы понять смысл грамотно поименованной функции, достаточно прочитать её имя. Чтобы понять смысл пронумерованной функции, нужно целиком прочитать её тело, т.к. смысл не всегда понятен по контексту вызова. Вообще, в развитых диалектах Рефала надобности во вспомогательных функциях гораздо меньше, чем в базисном Рефале (например, Рефале-2).

·         Использование чисел или литер вместо символов-слов. Например, функция может принимать одним из параметров или число 1, которое обозначает истину, или 0 (ложь). Правильнее вместо них использовать слова True и False. А ещё правильнее — вместо True/False по возможности использовать слова из предметной области: Found/NotFound, Simple/Complex, Changed/Intouch, Success/Errors

 

А какие вы можете назвать запахи кода и антипаттерны, характерные для Рефала?

 

С уважением,
Александр Коновалов

Sergei M. Abramov abram_AT_botik.ru

unread,
Dec 13, 2020, 8:30:02 PM12/13/20
to Александр Коновалов a.v.konovalov87_AT_mail.ru
День добрый, всем!

> А какие вы можете назвать запахи кода и антипаттерны, характерные
> для Рефала?

Все, что вскрывает (или скрывает?) недостатки используемого
представления объектных выражений и приводит к чудовищному (для
пониманию) кода:

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

2. Боязнь копирования кусков выражений в разные "дочерние" функции.

Этим страдают многия языки. Например, в Эрланге целый культ на тему
"стремись к хвостовой рекурсии!" и "пиши с аккумулятором, а потом
выдай результат, навесив на него reverse (Который, видимо, у них
написан на не Erlang-е)".

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

С уважением,

Абрамов С.М.
ab...@botik.ru
мобильный: +7(903)2928308

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 2:49:41 AM12/14/20
to re...@botik.ru
Доброе утро всем!

> 1. Выворачивание скобок наизнанку, для организации прохода по выражению.

К представлению объектных выражений этот приём (антиприём) не имеет отношение. Он, скорее, характерен для базисного Рефала (Рефала-2), в котором нет ничего подобного let-конструкциям (условия, перестройки, действия). И благодаря этому сквозному обходу исчезает нужда во вспомогательных функциях, можно обойтись одной.

Но, вообще этот приём уродлив, хоть и пропагандировался Турчиным.


> 2. Боязнь копирования кусков выражений в разные «дочерние» функции.

А в Рефале-5 ещё и в условия.

У меня в Рефале-5λ копирование выражений дорогое, но я копировать их по умолчанию не боюсь. Потому что потом я нахожу узкие места, изучая профиль программы, и устраняю лишние копирования уже в них.

А нет ли в Рефале Плюс другой боязни — боязни конкатенации?


Задача. Заменить в выражении все символы последовательными натуральными числами:

<Enum 'abc' (('de') 'f' ('gh') 'ij') 'k'> → 1 2 3 ((4 5) 6 (7 8) 9 10) 11

Решение на Рефале-5λ, эффективное (O(n)) и простое для понимания (на мой взгляд):

Enum {
e.Expr
= <DoEnum e.Expr 1> : e.Expr^ s.Num
= e.Expr;
}

DoEnum {
/* пусто */ s.Num = s.Num;

s.X e.Expr s.Num = s.Num <DoEnum e.Expr <+1 s.Num>>;

(e.Nested) e.Expr s.Num
= <DoEnum e.Nested s.Num> : e.Nested^ s.Num^
= (e.Nested) <DoEnum e.Expr s.Num>;
}

Конструкции = … : … в Enum и в последнем предложении DoEnum неявно транслируются в вызов вспомогательной функции. Знак ^ после имени переменной в образце означает, что она не повторная. Представление данных — плоское списковое, то самое с дорогим копированием и копеечной конкатенацией.

Как будет выглядеть O(n) решение в других диалектах Рефала?


С уважением,
Александр Коновалов

Andrei Klimov andrei_AT_klimov.net

unread,
Dec 14, 2020, 3:50:49 AM12/14/20
to re...@botik.ru
пн, 14 дек. 2020 г., 10:46 Александр Коновалов a.v.konovalov87_AT_mail.ru <re...@botik.ru>:
Доброе утро всем!

> 1.  Выворачивание скобок наизнанку, для организации прохода по выражению.

К представлению объектных выражений этот приём (антиприём) не имеет отношение.

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

Он, скорее, характерен для базисного Рефала (Рефала-2), в котором нет ничего подобного let-конструкциям (условия, перестройки, действия).

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

У меня в Рефале-5λ копирование выражений дорогое, но я копировать их по умолчанию не боюсь. Потому что потом я нахожу узкие места, изучая профиль программы, и устраняю лишние копирования уже в них.

Заниматься перепимыванием читабельной программы на нечитабельную - это, по-моему, как раз пример "антипаттерна". Так нарушается maitanability, сопровождаемость, развиваемость программы, что есть первое требование к хорошему программированию. Остальные требования, паттерны/антипаттерны вытекают из него. 

А нет ли в Рефале Плюс другой боязни — боязни конкатенации?

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


Enum {
  e.Expr
    = <DoEnum e.Expr 1> : e.Expr^ s.Num
    = e.Expr;
}

IMHO, это есть пример антипаттерна: повторное использование имени для нового значения. Я бы здесь обязательно написал е.Expr1. А то легко не понять при чтении и ошибиться при рефакторинге. 

Также избегаю повторения имени в популярных функ языках типа ML с let, ну, кроме совсем легко читаемых шаблонных случаев:

... 
let x = x+1 in
...

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

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

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

Андрей

Василий Стеллецкий swi_AT_cnshb.ru

unread,
Dec 14, 2020, 4:47:38 AM12/14/20
to re...@botik.ru
Добрый день всем!
Я про задачку...
Ну, на мой взгляд не просто для понимания...
На других диалектах Рефала - да пожалуйста!
В этой задаче удобно использовать Ящики или Копилку.
Например с копилкой:
a =/0/k/Enum/ 'abc' (('de') 'f' ('gh') 'ij') 'k'.
Enum s1e2=k/Enum1/k/ВК/'n'..k/Enum/e2.
     (e1)e2=(k/Enum/e1.)k/Enum/e2.
     =
Enum1 =/1/k/ЗК/'n='/2/.
      sn=snk/ЗК/'n='k/P1/sn..
Результат:
S:62 cc=0 M=1024
ПЗ: /0/ /1/ /2/ /3/ ( ( /4/ /5/ ) /6/ ( /7/ /8/ ) /9/ /10/ ) /11/
КОП: ( 'n' '=' /12/ )
(первый /0/ в поле зрения не имеет отношения к задаче, это мой интерфейс)

 
 
-- 
С уважением,
-- 
Василий Стеллецкий
 
 
 
14.12.2020, 10:47, "Александр Коновалов a.v.konovalov87_AT_mail.ru" <re...@botik.ru>:

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 5:11:07 AM12/14/20
to re...@botik.ru

Добрый день, Андрей!

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

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

 

«Заниматься переписыванием читабельной программы на нечитабельную — это, по-моему, как раз пример „антипаттерна“»

Согласен. Но если руководствоваться профилировщиком, корячить приходится не всю программу, а только условно 3 %.

 

«IMHO, это есть пример антипаттерна: повторное использование имени для нового значения. Я бы здесь обязательно написал е.Expr1. А то легко не понять при чтении и ошибиться при рефакторинге.»

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

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

Допустим, у нас есть некоторая сущность, которую нужно как-то обновить два раза и использовать:

F {
  … e.Entity …
    = <FirstUpdate e.Entity> : e.Entity^
    = <SecondUpdate e.Entity> : e.Entity^
    = <UseEntity … e.Entity …>
}

Допустим, нам теперь надо упростить структуру после обновлений. Добавляем одну строчку:

F {
  … e.Entity …
    = <FirstUpdate e.Entity> : e.Entity^
    = <SecondUpdate e.Entity> : e.Entity^
    = <Simplify e.Entity> : e.Entity^
    = <UseEntity … e.Entity …>
}

А теперь нам нужно между обновлениями привести её к канонической форме:

F {
  …
e.Entity
    = <
FirstUpdate e.Entity> : e.Entity^
    = <
Canonize e.Entity> : e.Entity^
    = <SecondUpdate e.Entity> : e.Entity^
    = <
Simplify e.Entity> : e.Entity^
    = <
UseEntity e.Entity …>
}

А с нумерацией возни будет больше. Исходный вариант:

F {
  …
e.Entity
   
= <FirstUpdate e.Entity> : e.Entity1
    = <SecondUpdate e.Entity1> : e.Entity2
    = <UseEntity … e.Entity2 …>
}

Теперь добавляем упрощение:

F {
  … e.Entity …
    = <FirstUpdate e.Entity> : e.Entity1
    = <SecondUpdate e.Entity1> : e.Entity2
   = <Simplify e.Entity2> : e.Entity3
    = <UseEntity … e.Entity3 …>
}

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

F {
  …
e.Entity
    = <
FirstUpdate e.Entity> : e.Entity1
    = <
Canonize e.Entity1> : e.Entity4
    = <SecondUpdate e.Entity4> : e.Entity2
   = <
Simplify e.Entity2> : e.Entity3
    = <
UseEntity e.Entity3 …>
}

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

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

Зачем нумеровать вручную, когда это может сделать сам компилятор?

Эта цепочка присваиваний компилятором переписывается во вспомогательные функции. Если бы программист вручную писал вспомогательные функции, то никаких номеров бы не было:

F {
  … e.Entity … = <F-FirstUpdated … <FirstUpdate e.Entity>>;
}

F-FirstUpdated {
  … e.Entity = <F-Canonized … <Canonize e.Entity>>;
}

F-Canonized {
  … e.Entity = <F-SecondUpdated … <SecondUpdate e.Entity>>;
}

F-SecondUpdated {
  … e.Entity = <F-Simplified … <Simplify e.Entity>>;
}

F-Simplified {
  … e.Entity = <UseEntity … e.Entity …>;
}

Поэтому номера — отстой!

Если уж не хочется переопределять имена, или язык не позволяет, то лучше давать понятные имена разным версиям:

F {
  … e.Entity …
    = <FirstUpdate e.Entity> : e.Entity-FirstUpdated
    = <Canonize e.Entity-FirstUpdated> : e.Entity-Canonized
    = <SecondUpdate e.Entity-Canonized> : e.Entity-SecondUpdated
   = <Simplify e.Entity-SecondUpdated> : e.Entity-Simplified
    = <UseEntity … e.Entity-Simplified …>
}

К слову, в Рефале Плюс переменные в перестройке совершенно спокойно перекрывают имена. Можно сказать, крышка ^ там подразумевается неявно.

 

««А нет ли в Рефале Плюс другой боязни — боязни конкатенации?»»
«Нет.»

Ну а как тогда Enum будет записан на Рефале Плюс? Если его дословно переписать на Рефале Плюс, не будет ли там квадратичной сложности?

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

 

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

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

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

 

С уважением,
Александр Коновалов

 

From: Andrei Klimov andrei_AT_klimov.net <re...@botik.ru>
Sent: Monday, December 14, 2020 11:50 AM
To: re...@botik.ru
Subject: Re: Запахи кода и антипаттерны в Рефале

 

пн, 14 дек. 2020 г., 10:46 Александр Коновалов a.v.konovalov87_AT_mail.ru <re...@botik.ru>:

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 5:25:44 AM12/14/20
to re...@botik.ru

Добрый день, Василий!

А если мы пишем для многопоточного Рефала, то статический ящик или копилка не подойдёт. Нужно использовать или динамический ящик, или передавать номер через параметр. Получится как-то так:

Enum e1 = k/DoEnum/e1 /0/.

DoEnum s1 e2 sN = sN k/DoEnum/ e2 k/P1/sN..
      (e1)e2 sN = k/DoEnum-Wrap/ k/DoEnum/ e1 sN. (e2).

DoEnum-Wrap e1 sN (e2) = (e1) k/DoEnum/ e2 sN.

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

 

С уважением,
Александр Коновалов

Василий Стеллецкий swi_AT_cnshb.ru

unread,
Dec 14, 2020, 5:52:50 AM12/14/20
to re...@botik.ru
Добрый день, Александр!
Да, шагов поменьше!
a =/0/k/Enum/ 'abc' (('de') 'f' ('gh') 'ij') 'k'.
Enum e1 = k/EndEnum/k/DoEnum/e1 /1/..
DoEnum s1 e2 sN = sN k/DoEnum/ e2 k/P1/sN..
      (e1)e2 sN = k/DoEnum-Wrap/ k/DoEnum/ e1 sN. (e2).
      sN = sN
DoEnum-Wrap e1 sN (e2) = (e1) k/DoEnum/ e2 sN.
EndEnum e1 sN = e1
 
S:35 cc=0 M=1024
ПЗ: /0/ /1/ /2/ /3/ ( ( /4/ /5/ ) /6/ ( /7/ /8/ ) /9/ /10/ ) /11/
КОП:
 
Спасибо, понял что такое "выворачивание скобок" :)
 
-- 
С уважением,
-- 
Василий Стеллецкий
 
 
 
14.12.2020, 13:25, "Александр Коновалов a.v.konovalov87_AT_mail.ru" <re...@botik.ru>:

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 7:51:48 AM12/14/20
to re...@botik.ru

Василий!

Выворачивание скобок, оно же сквозной просмотр, — это другое. Это использование стека для представления незакрытых скобок. Например, так:

Enum e1 = k/DoEnum/ /0/ ('$') e1 '$'.

*
Формат: k/DoEnum/ sN (tL eS) eU tR
*   eS —
просканированная часть
*   eU —
непросканированная часть
*   tL —
стек просканированных частей слева
*   tR —
стек непросканированных частей справа

*
символ просто переносим
DoEnum sN (tL eS) s1 eU tR = k/DoEnum/ k/P1/sN. (tL eS sN) eU tR.
*
в скобочный терм спускаемся
       sN (tL eS) (e1) eU tR = k/DoEnum/ sN ((tL eS)) e1 (eU tR).
* термы кончились, но стек не пустой
      
sN ((tL eS) e1) (eU tR) = k/DoEnum/ sN (tL eS (e1)) eU tR.
* термы кончились и стек тоже пустой — выход из цикла
       s
N ('$' eS) '$' = eS

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

e1 (e2 (e3 ^ e4) e5) e6

где знак ^ означает позицию, вызов функции DoEnum будет иметь вид

k/DoEnum/ ((('$' e1) e2) e3) e4 (e5 (e6 '$')).

Т.е. незакрытые скобки перед ^ оказываются закрывающими, а неоткрытые после ^ — открывающими. А скобка между e3 и e4 служит указателем ^.

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

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

Василий Стеллецкий swi_AT_cnshb.ru

unread,
Dec 14, 2020, 8:05:24 AM12/14/20
to re...@botik.ru
Александр!
Большое спасибо за подробный рассказ!
Но, то что в примере применили Вы - тоже самое: Справа стек.
Только просмотренную часть Вы не собирали в левой скобке, а сразу выводили за пределы функции...
Точнее стек получался из вызовов DoEnum-Wrap и скобок наизнанку ...
Еще раз, Большое спасибо!
P.S. А в этом варианте получился 31 шаг.
4 шага где-то сэкономили ;)
 
-- 
С уважением,
-- 
Василий Стеллецкий
 
 
 
14.12.2020, 15:23, "Александр Коновалов a.v.konovalov87_AT_mail.ru" <re...@botik.ru>:

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 8:38:49 AM12/14/20
to re...@botik.ru

Василий!

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

a =/0/k/Enum/ 'abc' (('de') 'f' ('gh') 'ij') 'k'.
Enum e1 = k/EndEnum/k/DoEnum/e1 /1/..
DoEnum s1 e2 sN = sN k/DoEnum/ e2 k/P1/sN..

      (e1)e2 sN = k/DoEnum-Wrap/ (k/DoEnum/ e1 sN.) e2.
      sN = sN
DoEnum-Wrap (e1 sN) e2 = (e1) k/DoEnum/ e2 sN.


EndEnum e1 sN = e1

И нету оборачивания скобки в рекурсивном решении.

Один шаг сэкономился на EndEnum, т.к. в последнем предложении мы и стеки отбрасываем, и счётчик. Ещё шаги экономятся на обработке скобок. Открывающая скобка в обоих вариантах требует одного шага. Закрывающая скобка — двух шагов: последнее предложение DoEnum, где она видит аргумент с пустой строкой и счётчиком и DoEnum-Wrap, которая выбрасывает вон скобку и продолжает цикл DoEnum. Но закрывающих скобок 4, должно сэкономиться 4 шага. И один на EndEnum. 4+1≠4, где-то что-то я не учёл.

Василий Стеллецкий swi_AT_cnshb.ru

unread,
Dec 14, 2020, 9:01:02 AM12/14/20
to re...@botik.ru
Александр.
Действительно.
Еще раз, Большое спасибо.
PS Если хотите, могу прислать пошаговую прокрутку обоих вариантов...
 
-- 
С уважением,
-- 
Василий Стеллецкий
 
 
 
14.12.2020, 16:38, "Александр Коновалов a.v.konovalov87_AT_mail.ru" <re...@botik.ru>:

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 9:09:15 AM12/14/20
to re...@botik.ru

Василий, не надо пошаговую прокрутку. Я неправильно посчитал скобки. Во входной строке их не 4, а 3, поэтому всё сходится: 3+1=4.

Arkady Klimov arkady.klimov_AT_gmail.com

unread,
Dec 14, 2020, 11:49:18 AM12/14/20
to re...@botik.ru
Я тоже начал было писать про "настоящее выворачивание", но Александр меня опередил.
Да, это оно самое. Мне такой стиль не нравится, и сам я так не пишу. Возможно, потому, что Рефал-6 позволяет не экономить на копированиях, и я предпочитаю рекурсивный стиль. Но хочу высказаться в защиту этой формы, которую использовал Турчин. Думаю, тут дело не в экономии копирований, а в стиле мышления. Как ни странно, Турчин рассматривал работу рефал-программы как пошаговый процесс с изменяющимся состоянием. Отсюда и понятие поля зрения. Да, оно моделирует стек, неявно возникающий при рекурсии. Но при мета-деятельности Турчин предпочитал поле зрения объектной программы держать как бы целиком перед глазами. В центре - активная конкретизация, а по краям - окружение. Отсюда и возник такой стиль. Он в каком-то смысле сближает рефал с машиной Тьюринга, только вместо линейной ленты - дерево.
Когда-то около 90 года я попытался с ним на эту тему заговорить, как бы предлагая перейти на рекурсивный стиль, но он как-то это сходу отверг, мол так ему удобнее. Разговора по существу не получилось. Поэтому сейчас я его так и понимаю, как написал выше.

С уважением, 
Аркадий

пн, 14 дек. 2020 г. в 16:08, Александр Коновалов a.v.konovalov87_AT_mail.ru <re...@botik.ru>:


--
_______________
С уважением,
Аркадий Климов,
с.н.с. ИППМ РАН,
+7(499)135-32-95
+7(916)072-81-48

nikolai.kondratiev_AT_gmail.com

unread,
Dec 14, 2020, 2:35:35 PM12/14/20
to re...@botik.ru

Я вспоминаю, что Турчин был очень доволен тем, что нашел алгоритм СПАРИВАНИЯ СКОБОК, работающий линейное время, а сквозной просмотр появился как вариация на ту же тему.

Мне кажется, что сам сквозной просмотр он не слишком пропагандировал.

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 5:06:33 PM12/14/20
to re...@botik.ru

Доброй ночи, Аркадий!

Спасибо за пояснение про намерения Турчина! Это действительно интересно.

На самом деле, решение

Enum e1 = k/EndEnum/k/DoEnum/e1 /1/..
DoEnum s1 e2 sN = sN k/DoEnum/ e2 k/P1/sN..
      (e1)e2 sN = k/DoEnum-Wrap/ (k/DoEnum/ e1 sN.) e2.
      sN = sN
DoEnum-Wrap (e1 sN) e2 = (e1) k/DoEnum/ e2 sN.
EndEnum e1 sN = e1

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

 

С уважением,
Александр Коновалов

Александр Коновалов a.v.konovalov87_AT_mail.ru

unread,
Dec 14, 2020, 5:17:18 PM12/14/20
to re...@botik.ru

Доброй ночи, Николай!

Сквозной просмотр был у Турчина в учебнике по Рефалу-5:

Глава 3.   Основные приемы программирования (koi8-r) (refal.ru)

Да, и одним из примеров является спаривание скобок.

Ещё этот приём используется в суперкомпиляторах SCP3 и SCP4. Можно исходники скачать и убедиться.

 

С уважением,
Александр Коновалов

Reply all
Reply to author
Forward
0 new messages