Skip to content
Alexey Yurchenko edited this page Apr 14, 2023 · 7 revisions

A brief introduction to Novika

Behind Novika

The word block you will hear very often in any Novika conversation.

Blocks are primary in Novika the Set of Ideas.

Novika the Language is a convenient way to write these ideas down, in a human-readable form, and to parse and manipulate them.

Finally, Novika the Interpreter is the expert on bringing blocks and all sorts of other ideas to life, on a personal computer. The interpreter makes these ideas useful for someone who does programming rather than simply discusses it.

These three components of Novika The Project are not separate. They interact with each other. They "vote" on ideas and propose new ones to reach project-wide equilibrium, filling conceptual gaps or gaps in the machinery as the project progresses.

Long time ago, there was no such thing as a stack in Novika the Set of Ideas. Then came my decision to use the postfix notation for Novika the Language.

The most convenient and popular way to work with the postfix notation is to combine it with a data structure called stack. Shortly thereafter, I introduced and implemented stacks in Novika the Interpreter.

The more and more I worked on stacks, the more footprint they left on Novika the Set of Ideas. They generated idea after idea, further influencing and balancing the other parts of Novika the Project.

This is only a single example of the interactions between the three parts of Novika. There are many more, although I personally might not be aware of them.

Comparing to other languages

In my opinion, the best way to compare Novika to other languages is to compare it component-wise.

  • Novika the Set of Ideas intersects heavily with JavaScript's.
  • Novika the Language is similar to that of most Lisp dialects, Forth, and Factor.
  • Novika the Interpreter... well, it's just a pretty naïve interpreter. No other mainstream language I know of has such a crappy interpreter :)

Non-linearity of documentation

I find it very hard to describe Novika linearly, from the bottom to the very top. The reason is that many concepts are recursive, or assume the reader knows about something that will only become explainable later on.

What this means for you personally is that you should skim over terms you do not understand. By "skim" I mean you should mark them as "variables" or "holes" to fill in with knowledge later. These "holes" are constrained by the paragraphs around them. The more you read, the more constraints you collect. With enough constraints, you should be able to understand the terms.

In other words, you should try to infer the words that you do not understand while reading rather than trying to get them the first time.

Blocks: a forward declaration

What blocks do is they glue forms together.

  1. One way to glue two forms, let's say A and B, is to connect them in a (unidirectional) association relationship, as in hash tables. What I mean by this is that when the A form is seen, the B form is assumed.

  2. Another way to glue two forms is to create a sequence, for example by saying that the A form precedes the B form (and/or that the B form follows the A form).

Tape

The tape of a block glues forms sequentially and bidirectionally. This is expressed by simply listing the forms — one after another, separated by whitespace, as in A B or 1 2 3.

Cursor

The tape also offers a "cutpoint" or insertion point called the cursor, and in most Novika words it is abbreviated as simply |. You can move the cursor left and right programmatically, similarly to how you can do it in a textbox or an HTML input element. Although things do get a lot more complex and high-level with words like |afterOrToEnd and |hydrate!

Dictionary

The dictionary of a block allows to create association relationships between forms. The individual associations (as in "when you see A, assume B") are called dictionary entries, or entries for short.

There are two kinds of dictionary entries — and therefore, two kinds of association relationships.

Opener entries

The first kind of association relationships is the opens relationship. A dictionary entry that has its two forms in an opens relationship is called an opener entry.

Opener entries are generally created using the word @:. Try typing help @: in the REPL.

Pusher entries

The second kind of association relationships is the pushes relationship. A dictionary entry that has its two forms in an pushes relationship is called a pusher entry.

Pusher entries are generally created using the word $:. Try typing help $: in the REPL.

Submission to an entry

Sometimes it is useful to change the B form in an association relationship. That is, in the phrase "when you see A, assume B" we may want to replace B with C. In the context of Novika and dictionary entries, this is called submission to an entry. Note that the kind of association relationship is not changed, what changes is only the B or the right-hand side of it.

The word =: is generally used to submit to an entry. Try typing help =: in the REPL.

What are forms?

Form is a generic name (and a kind of supertype) given to a suite of data types and literals provided by Novika. Note that there is no distinction between parse-time and runtime in Novika. This is to say a form is a literal and an AST node as well as a data type that you operate on at runtime.

Blocks are also forms, making it possible to glue blocks to blocks in sequential and/or associative relationships as described above, by turning to higher and higher-order blocks.

Value forms

In fact, there are two kinds of forms: value forms, and blocks. Let's take a look at the kinds of value forms present in Novika.

Decimal

Decimal is the main and very high-level number type. There are no floats, no doubles, no integers and no bytes in Novika. In exchange for performance, there is only the decimal type. It allows you to express arbitrarily large or small numbers and do precise computations on them, effortlessly.

>>> 1 2 +
[ 3 ]
>>> 2 2 * 2 +
[ 3 6 ]
>>> 0.1 0.2 +
[ 3 6 0.3 ]
>>> *
[ 3 1.8 ]
>>> +
[ 4.8 ]

Quote

Quotes are the strings of Novika. They are named so because they are essentially raw, uninterpreted (although this isn't exactly true) quotations of the source code.

>>> 'Hello World!'
[ 'Hello World!' ]
>>> ' Hello from Novika!'
[ 'Hello World!' ' Hello from Novika!' ]
>>> ~
[ 'Hello World! Hello from Novika!' ]
>>> echo
Hello World! Hello from Novika!
[ ]

Boolean

Booleans are probably the simplest kind of form. Normally written as true or false, Novika booleans stand for whether an expression is truthy or falsey

>>> [] empty?
[ true ]
>>> [1 2 3] all: even?
[ true false ]
>>> and
[ false ]

Color

Exactly what the name suggests, color forms represent RGBA colors. The alpha component is often omitted to get an opaque color. Various color and color space conversion words exist that take and/or leave color forms, for instance 0 255 0 rgb getLCH.

Builtin

Builtins are native code as seen from Novika. They implement performance-critical functionality or functionality that cannot be implemented in Novika directly. For example, + is a builtin. You can check this for yourself by typing #+ here in the REPL. You will see that it's [native code for: '+']. You can follow with typedesc if you're still unsure: it will confirm that it's a builtin after all.

>>> 1 2 + echo
3
[ ]
>>> "Hmm, I wonder how + is implemented..."
>>> #+ here
[ [native code for: '+'] ]
>>> typedesc
[ 'builtin' ]

Word

If you open any Novika source file, almost everything you will see there is words and block brackets []. Words are the forms which most often participate as the left-hand side in association relationships. That is, in the phrase "when you see the A form, assume the B form", the A form is most probably a word form (and the B form is most probably a block, by the way).

Words are just sequences of characters. Almost any characters, in fact, other than whitespace. For instance, swap, 1AmA-zuůk, ± or even {foobra=zoobra} are all single words.

During parsing, word is the fallback literal. That is, instead of getting parse errors or something like that, you will get words. A missing (or misplaced) whitespace, wrong kind of character — anything otherwise unrecognizable by the Novika classifier (or scissors) becomes a word.

Quoted word

When a block is opened, all words in it are consecutively opened as well.

To counter that, you may want to quote your word. Unless you have to quote at runtime (which is possible, of course), you'll find the quoted word literal useful.

For example, in #+ here we must quote the word + because we don't want to add there (which would be the case were the + word opened without the # which quotes it). Rather, we want to refer to the word — so that perhaps whomever we give it to (here in our case) can use it somehow, for instance to read the associated ("B") form as here does.

In other words, by using # we talk about words as symbols rather than words as things that they stand for.

Note that quoted words do not keep track of the block the resulting word exists in. They are simply literals, that's it. Giving the quoted word #foobra to another block may end up with a different meaning for the word foobra were the recipient block resolve it.

The amount of # (depth of quotation) is not limited in any way. That is, ##foobra, when opened, will become #foobra. On the next open it in turn will become the word literal foobra.

Words obtained from quoted words cannot be opened using open. This restriction exists to underline the fact that words are unaware of their block of origin.

To open a word literal at runtime, you must either enclose it in a block and open that block, or use words such as entry:open or here which explicitly require or explicitly assume a block where the associated form should be searched for.

The easiest one is here, but there are some caveats.

>>> 100 $: foobra
[ ]
>>> ##foobra
[ #foobra ]
>>> open
[ foobra ]
>>> open
[ foobra ] "That's it, no more quoting."
>>> here
[ 100 ]