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.

    image

    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.

    Also popular now: