Stack Programming with a Human Face (Part Two)
As expected, the previous post caused conflicting comments. Someone is satisfied with the existing Fort to resolve issues, someone (like me) is annoyed by its features.
Let's put all the dots over i right away: I'm not trying to write a replacement for Fort. Fort is a family of mid-level programming languages that continues to efficiently solve assigned tasks and is not going to retire. But I'm thinking in a different niche: a high-level stack language with emphasis on the ease of reading programs for beginners (as much as possible). The great traditionality and high level has its advantages, but at the same time some of the Fort's features (including positive ones) are lost.
The new imaginary language has its own philosophy and its own concepts. I will continue to write about this.
Every creator of a language in which there is not only recursion, sooner or later thinks about a set of cyclic constructions. I prefer to first consider the design of the general universal cycle from which the rest can be derived, and then, based on practical experience, add additional constructions for the most common cases.
The main loop looks like this:
Scary, right? But in fact, this is only a formal description, everything works in an elementary way. Every iteration, all any-words are executed after repeat. Then the calculation of the when conditions occurs. If condition1 is true, then words1 are satisfied and the cycle starts a new iteration from the very beginning. If condition1 is false, then the transition to the next condition occurs. If none of the conditions is true, a branch is executed otherwise.
This cycle is good in that you can get anything you want out of it, that is, it is a common basic construction.
1. Infinite loop (optional when and otherwise omitted, they are not needed):
It is assumed that somewhere inside the loop there will be an if with a termination condition and the word exit-repeat.
2. A cycle with a precondition:
3. The cycle with the postcondition:
4. The loop with the counter (take the integer variable counter as an example):
5. The cycle with the output in the middle:
6. And even the Dijkstra cycle!
Let's see what happened. The endless cycle is intuitive and concise, no extra words, so just leave it as it is. A cycle with a postcondition is less common for and while, so there is no point in a separate construction. If such a cycle is still needed, it can be easily deduced from the general construction, since it turned out to be clear and concise.
But the cycles with the precondition and with the counter turned out to be more awkward. Since they are often needed, it makes sense to implement them in the form of separate words:
1. A cycle with a precondition:
2. Cycle with counter:
3. And a cycle with a postcondition (with a great desire):
Pay attention to an important point: while and loop can be implemented as a simple text substitution. Indeed, if you replace while while with “repeat when”, and end-while with “otherwise exit-repeat end-repeat”, you get a common loop. Similarly, a loop with a postcondition: loop on “repeat”, until on “”, and end-loop on “when not do exit-repeat end-repeat”. The repeat loop itself can be converted into an if set if desired.
That is, we need to implement only two loops in the translator: repeat and for. While and loop loops can be done on textual substitutions using the language itself. A similar approach is adopted by Eiffel and Lisp: we have a generalized construction (for example, loop in Eiffel and cond in Lisp), which can be used very flexibly. If possible, new designs are implemented on top of the old. In Fort, the principle is the opposite: we have a lot of special cases and tools with which we can create the necessary structures ourselves if necessary.
Each approach has its pros and cons. The approach from “general to particular” is good in high-level programming, because the programmer does not have to look into the “offal” of the translator and be wiser with the implementation of the next bike. In Fort, you will first have to study the insides of the fort system and enter new words, but then it becomes possible to use the new word unlimitedly, executed quickly and efficiently. Not that one approach is better than the other, they are just different. Since I care about the readability of the code and the simplicity for beginners, I took the first approach as the main one.
We will do the same with conditional expressions. Generalized construction (hello, Lisp!):
Cond works similarly to repeat, but only once, and not repeatedly. For early exit, the word exit-cond is provided. Cond can also be inferred from repeat: you just need to put exit-repeat after each when-branch. So that!
Although cond can be used for branching of any complexity, it’s useful to implement some common patterns separately.
1. The simplest conditional operator:
2. But the case construct is specific to stack programming. Suppose we have some kind of variable x and we need to execute a certain cond branch depending on the value of x. Before each when in cond or before each if (depending on the implementation method) we have to put dup, that is, take care of duplication and / or drop of extra elements:
The "noise" words here are completely superfluous. Indeed, if such situations occur regularly, why not automate dupes and drops, increasing readability and conciseness? And if we need to compare two variables? And if three? This is how much you have to wise with the stack before each condition!
Especially for such situations, you need a case construct - a more “smart” version of cond. The syntax is very similar:
The main difference is that each time before when the compared elements are doubled, and before the end-case, the excess copies are removed from the stack. That is, case = cond + apart dup and drop. The number of-compared-elements indicates how many elements on the top of the stack need to be doubled:
Well, we already have the building blocks. There was a solution - the definition of new words. Such words are called through call, in terms of assembly language:
But such words work through textual substitution: the name is replaced by the body. As you might guess, while, loop and if can be implemented on macros as follows:
We introduce another familiar word for beauty:
Now you can write something very concise and clear
Total: thanks to the flexible constructions of repeat and cond with the help of elementary text substitution, you can implement a whole set of building blocks for all occasions. Unlike Fort, you don’t have to think about the implementation of words at all, we abstract from the implementation details.
For example, the word when in cond and repeat is more cosmetic in nature: it allows you to visually separate the condition from other words. But in fact, only the word do plays some role. Want extreme brevity?
and write
completely without thinking about the implementation of the translator. This is not our concern, we are at a high level!
Next time, the conversation will be about the compiler parsing the source code of the program, about the stacks, data and variables.
Let's put all the dots over i right away: I'm not trying to write a replacement for Fort. Fort is a family of mid-level programming languages that continues to efficiently solve assigned tasks and is not going to retire. But I'm thinking in a different niche: a high-level stack language with emphasis on the ease of reading programs for beginners (as much as possible). The great traditionality and high level has its advantages, but at the same time some of the Fort's features (including positive ones) are lost.
The new imaginary language has its own philosophy and its own concepts. I will continue to write about this.
Selection of loop designs
Every creator of a language in which there is not only recursion, sooner or later thinks about a set of cyclic constructions. I prefer to first consider the design of the general universal cycle from which the rest can be derived, and then, based on practical experience, add additional constructions for the most common cases.
The main loop looks like this:
repeat
любые-слова
when условие1 do слова1
when условие2 do слова2
when условиеN do словаN
otherwise ветвь-иначе
end-repeat
Scary, right? But in fact, this is only a formal description, everything works in an elementary way. Every iteration, all any-words are executed after repeat. Then the calculation of the when conditions occurs. If condition1 is true, then words1 are satisfied and the cycle starts a new iteration from the very beginning. If condition1 is false, then the transition to the next condition occurs. If none of the conditions is true, a branch is executed otherwise.
This cycle is good in that you can get anything you want out of it, that is, it is a common basic construction.
1. Infinite loop (optional when and otherwise omitted, they are not needed):
repeat
любые-слова
end-repeat
It is assumed that somewhere inside the loop there will be an if with a termination condition and the word exit-repeat.
2. A cycle with a precondition:
repeat
when условие-истинно? do тело-цикла
otherwise exit-repeat
end-repeat
3. The cycle with the postcondition:
repeat
тело-цикла
when условие-выхода do exit-repeat
end-repeat
4. The loop with the counter (take the integer variable counter as an example):
repeat
counter @ (проверка счётчика)
when 100 < do
|counter @ ++| counter set (увеличиваем на единицу счётчик, если он меньше 100)
тело-цикла
otherwise exit-repeat
end-repeat
5. The cycle with the output in the middle:
repeat
какой-то-код
when выходим? do exit-repeat
otherwise продолжаем
какой-то-другой-код
end-repeat
6. And even the Dijkstra cycle!
Let's see what happened. The endless cycle is intuitive and concise, no extra words, so just leave it as it is. A cycle with a postcondition is less common for and while, so there is no point in a separate construction. If such a cycle is still needed, it can be easily deduced from the general construction, since it turned out to be clear and concise.
But the cycles with the precondition and with the counter turned out to be more awkward. Since they are often needed, it makes sense to implement them in the form of separate words:
1. A cycle with a precondition:
while условие do
слова
end-while
2. Cycle with counter:
for имя-переменной начальное-значение to конечное-значение step шаг do
слова
end-for
3. And a cycle with a postcondition (with a great desire):
loop
слова
until условие-выхода
end-loop
Pay attention to an important point: while and loop can be implemented as a simple text substitution. Indeed, if you replace while while with “repeat when”, and end-while with “otherwise exit-repeat end-repeat”, you get a common loop. Similarly, a loop with a postcondition: loop on “repeat”, until on “”, and end-loop on “when not do exit-repeat end-repeat”. The repeat loop itself can be converted into an if set if desired.
That is, we need to implement only two loops in the translator: repeat and for. While and loop loops can be done on textual substitutions using the language itself. A similar approach is adopted by Eiffel and Lisp: we have a generalized construction (for example, loop in Eiffel and cond in Lisp), which can be used very flexibly. If possible, new designs are implemented on top of the old. In Fort, the principle is the opposite: we have a lot of special cases and tools with which we can create the necessary structures ourselves if necessary.
Each approach has its pros and cons. The approach from “general to particular” is good in high-level programming, because the programmer does not have to look into the “offal” of the translator and be wiser with the implementation of the next bike. In Fort, you will first have to study the insides of the fort system and enter new words, but then it becomes possible to use the new word unlimitedly, executed quickly and efficiently. Not that one approach is better than the other, they are just different. Since I care about the readability of the code and the simplicity for beginners, I took the first approach as the main one.
Conditional Constructions
We will do the same with conditional expressions. Generalized construction (hello, Lisp!):
cond
when условие1 do ветвь1
when условие2 do ветвь2
when условиеN do ветвьN
otherwise ветвь-иначе
end-cond
Cond works similarly to repeat, but only once, and not repeatedly. For early exit, the word exit-cond is provided. Cond can also be inferred from repeat: you just need to put exit-repeat after each when-branch. So that!
Although cond can be used for branching of any complexity, it’s useful to implement some common patterns separately.
1. The simplest conditional operator:
условие if
ветвь1
else
ветвь2 (конечно, необязательная)
end-if
2. But the case construct is specific to stack programming. Suppose we have some kind of variable x and we need to execute a certain cond branch depending on the value of x. Before each when in cond or before each if (depending on the implementation method) we have to put dup, that is, take care of duplication and / or drop of extra elements:
: testif
dup 1 = if ." One" else
dup 2 = if ." Two" else
dup 3 = if ." Three"
then then then drop ;
The "noise" words here are completely superfluous. Indeed, if such situations occur regularly, why not automate dupes and drops, increasing readability and conciseness? And if we need to compare two variables? And if three? This is how much you have to wise with the stack before each condition!
Especially for such situations, you need a case construct - a more “smart” version of cond. The syntax is very similar:
case число-сравниваемых-элементов
when условие1 do ветвь1
when условие2 do ветвь2
when условиеN do ветвьN
otherwise ветвь-иначе
end-cond
The main difference is that each time before when the compared elements are doubled, and before the end-case, the excess copies are removed from the stack. That is, case = cond + apart dup and drop. The number of-compared-elements indicates how many elements on the top of the stack need to be doubled:
x @
y @
case 2 [x y -- x y x y]
when = do "равны" print-string
when < do "y больше" print-string
otherwise "x больше" print-string
end-case
Enter new words and describe the substitution
Well, we already have the building blocks. There was a solution - the definition of new words. Such words are called through call, in terms of assembly language:
define имя тело end
define-macro имя тело end-macro
But such words work through textual substitution: the name is replaced by the body. As you might guess, while, loop and if can be implemented on macros as follows:
define-macro while
repeat when
end-macro
define-macro end-while
otherwise exit-repeat
end-repeat
end-macro
define-macro loop
repeat
end-macro
define-macro until
when
end-macro
define-macro end-loop
not do exit-repeat
end-repeat
end-macro
define-macro if
cond when do
end-macro
define-macro else
otherwise
end-macro
define-macro end-if
end-cond
end-macro
We introduce another familiar word for beauty:
define-macro break!
do exit-repeat
end-macro
Now you can write something very concise and clear
repeat
...
when 666 = break!
...
end-repeat
Total: thanks to the flexible constructions of repeat and cond with the help of elementary text substitution, you can implement a whole set of building blocks for all occasions. Unlike Fort, you don’t have to think about the implementation of words at all, we abstract from the implementation details.
For example, the word when in cond and repeat is more cosmetic in nature: it allows you to visually separate the condition from other words. But in fact, only the word do plays some role. Want extreme brevity?
define-macro ==>
when do
end-macro
and write
cond
x @ y @ = ==> "равны"
x @ y @ < ==> "y больше"
otherwise "x больше"
end-cond
print-string
completely without thinking about the implementation of the translator. This is not our concern, we are at a high level!
Next time, the conversation will be about the compiler parsing the source code of the program, about the stacks, data and variables.