Nomenine is a homo-iconic, object-oriented programming language with pattern matching, multiple inheritance and polymorphism.
Most objects are immutable. Nomenine handles similarly to functional programming languages in many respects. There is also room for mutable objects and imperatives.
Everything in Nomenine is an object. Objects are closures that capture their attributes. The object as a function takes a context containing a message as its only parameter. It deals with the message, and sends a responds message to the context.
Nomenine is a dynamically typed language. Clauses are used to control pattern matching which is used to control polymorphism. Clauses can be seen as the syntactic equivalent of types.
Range and Kleene star are among the available clauses. Clauses can be defined in the language itself. The parser is a clause.
Nomenine is not compiled, but it is not really an interpreted language either. There is no interpretation loop with a switch in it. The interpretation of an expression is the result of a ricochet of messages sent between objects. The sending of a message is also the result of a ricochet of messages sent between objects. This causes circularity, but Nomenine handles this circularity automatically, allowing every component of the system to behave as perfectly ordinary high-level user-space components.
Built-in components and user-space components are always interchangeable.
Other especially noteworthy features are lazy objects and maps, promises and map/reduce. There are other significant features planned.
This is what it looks like:
let sort ( function ( List ) [ let l ( : that next ) [ if pivot [ : that value ] then [ merge ( sort ( filter ( l ) [ : that =< ( pivot ) ] ) ) ( list ( pivot ) ) ( sort ( filter ( l ) [ : that > ( pivot ) ] ) ) ] else [ list ] ] ] ) [ sort ( list 4 2 5 7 4 2 ) ]
This chapter gives an overview of syntax and semantics without going into any details.
Nomenine syntax does not look very different from most languages:
someNumber + 1
Expressions uses parenthesis:
someNumber + ( someOtherNumber * 10 )
Object attributes are not accessed using dot-notation. Instead the components of a path is simply separated by spaces:
someObject someAttribute someAttributeOfThatAttribute
Expressions are paths. However a component of a path may be a pattern that spans many components of an expression.
Since expressions are paths, there must be an object at the beginning of that path. This object is the scope.
x + ( y )
Notice the use of parenthesis around y. This parenthesis is not optional. The reason is that y is acquired from scope. Parenthesis are not optional, they either must be included or must be omitted. The important point here is that expressions are paths that originates from the scope object.
Nomenine is very focused on consistency. So much in fact that integer literals have no special meaning in scope. To get a number, use the identity function .:
. 2 + 2
Will return 4.
Similarly, words and quoted expressions can be quoted. Expressions can not be quoted because they are evaluated implicitly:
. ( a + 1 )
Will return the result of
a + 1 and not the expression object itself.
It is equivalent to:
a + 1
Functions can be created using function:
function ( Integer ) [ : that * 2 ]
Notice the use of [ ]. The brackets are used to quote code. The quoted code becomes the body of the function. Quoted code is an ordinary list object.
Notice in the last example the use of :. : refers to the context.
The word that refers to the message object in the context. Similarly there is a word this which is used as expected.
Integer in the previous example is a Clause object. It says that the function takes an integer as an argument. The role of clause objects in Nomenine is similar to types or classes in many other languages.
However clause objects are more about the pattern matching then types. Clause objects can consume more than one object in the phrase, and use the consumed objects to produce a result in any way.
The main types of clause objects are:
- This is Kleene Star. It will consume all objects of a given type, and return them as a list.
- Plus is similar to Star only that it requires the list of matched objects to be non-empty.
- This consumes a objects matching a particular pattern of Clause objects. A list of matches is returned.
- This is similar to Grouping except that it returns a Union of the matches.
- This is used with Pattern to name matches. Using Shape one can refer to specific matches in a pattern by name. This is how multiple parameters to functions are handled.
- any word
- A word object will consume/match itself. This way words can be used in patterns.
When building a function, the objects between function and the quoted expression are used to form a pattern. In the following function, the pattern consists of the word f followed by the Integer clause object:
function f ( Integer ) [ : that + 4 ]
Objects are functions and objects that understand more than one type of message are polymorphic functions. Polymorphic functions are just unions of functions. Using words as clauses makes it possible to use the components of a polymorphic function as a method.
The following object is a union of two functions:
union ( function f1 ( Integer ) [ : that + 2 ] ) ( function f2 ( Integer ) [ : that + 4 ] )
To use the first function, one sends a message like
f1 123 to the object.
To use the other function, one sends a message like
f2 123 to the object.
The fact that the words f1 and f2 are part of the patterns of each function makes it possible to distinguish between them.
Let and with
Most objects are immutable. It is therefore natural to use let or similar:
let someInteger ( 123 ) [ someInteger + 2 ]
Notice that the type of someInteger is not specified.
The object oriented version of let becomes with. It is similar to let*. It is possible to have more than one definition:
with ( union ( function f ( Integer ) [ : that + 4 ] ) ( function x [ . 100 ] ) ) [ f ( x ) ]
with takes an object and a list as its arguments. The list is the expression. The expression will be evaluated in a scope that is the union of the scope and the object, so that terminology both from the object and the scope is available. Terminology in the object will overshadow same terminology in the scope.
If-statements are expressions. Also, there is no booleans or boolean logic. If the condition fails, the else-branch is taken, and if the condition produces a result, the then-branch is taken.
if [ x < 10 ] then [ y ] else [ z ]
It is possible to name that result and refer to it in the then-branch:
Here y is returned if
x < 10 does not fail.
Otherwise z is returned.
There are sparse facilities for mutable objects and imperatives:
do [ x = 10 ] do [ y = ( x + 2 ) ]
do evaluates the expression given and returns its this, which is usually scope. In order for the assignments to work, the objects x and y must understand those messages.
There are currently no loop mechanism, but it is possible to use Range and reduce
Range @ 1 9 reduce 1 [ : this + ( : that ) ]
This calculates the factorial of 9.
: this refers to the sum while
: that refers to the current element.
Also notice that @ is the equivalent of new as used in object-oriented programming languages.
This chapter gives an in-depth explanation of the different aspects of the Nomenine programming language.
7.1 If statements
There are two main forms of if-statements.
1. Regular if-then with optional else-clause
- if [ condition ]
- if [ condition ] then [ value ]
- if [ condition ] then [ value ] else [ alternative-value ]
2. The if-let statement where the result of the condition, named variable, can be used in the then-clause.
- if variable [ condition ]
- if variable [ condition ] then [ value ]
- if variable [ condition ] then [ value ] else [ alternative-value ]
Notice that there are no booleans. Instead, the then-clause is evaluated if the condition-clause does not fail, and if it does fail, the optional else-clause is evaluated.
An if-expression is similar to a try-clause. The if-statement without either a then-clause or else-clause simply prevents a fail message from propagating.