A simple message-based programming language inspired by Smalltalk, Self, Erlang, Clojure, sci-fi and biology.
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
– Alan Kay
The basic principle of recursive design is to make the parts have the same power as the whole.
– Bob Barton
A system of cells interlinked within cells interlinked within cells interlinked within one stem.
– K, Blade Runner 2049 (Vladimir Nabokov, Pale Fire)
-- method definition
hello: '(name)' -> "hello, {name}!"
-- message pipeline
hello "world" | print
--> "hello, world!"
Cells encapsulate fields (state), receptors (methods) and messages (expressions), and communicate by message signaling. Messages are matched against the signatures of the receiving cell's receptors. The receptor is responsible for the transduction of a received message and for producing a response.
There are no classes, only cloning (by concatenation/mixin or delegation), composition and protocols. The ancestors and descendants of a cell is recorded.
Cells are encapsulated, their internal state may only be accessed from the outside through messaging, with capability-based security. Exceptions are handled internally, a cell should not be able to crash the system.
Cells are first-class reference types that are passed by value. They have lexical closure and may observe each other, enabling reactivity.
Summary: A cell is the consolidation of object, function and block, implemented as a first-class reference type, communicating by messaging. Encapsulated and safe.
Fields hold the cell's internal state. Fields are read-only, but the bound value may be writable. Fields are lexically scoped, only directly accessible from the current and any nested scopes.
Summary: Fields are the consolidation of block-scoped variables and object properties.
An expression contains one (or more) literal(s) or signal(s). If comma separated, it evaluates to the result of the last item.
A signal is a message sent to a cell. Expressions are used to include values as arguments in message slots. Messages are dynamically dispatched, with the ability to support multiple dispatch.
There are no statements, only cells (senders and receivers) and expressions (message signals).
Summary: Everything is an expression, and every expression is the sending of a message (or messages).
{}
– cellnothing
– bottom value (a cell that only ever returns itself)
boolean
–true
orfalse
number
– IEEE 754 64-bit double-precision floating-point?string
– UTF-8?[]
– collection
Collection is the consolidation of indexed array (list/vector) and associative array (object/dictionary/structure), similar to Lua's tables. Collections are implemented as persistent data structures.
Value types are immutable. If marked as writable, the value will be wrapped in a Value
cell, similar to Clojure's atoms. This allows management of state over time, while enabling validation and subscription to events.
Observing a Value
will automagically dereference it, returning a snapshot of its current state (value). Mutating a Value
will swap the old immutable value for a new one. Collections use structural sharing of past states.
{}
cell[]
collection42
number""
string''
message definition()
message parameter, expression->
methodtrue
false
nothing
|
pipeline»
compose left-to-right«
compose right-to-left,
expression separator
*
mutable_
ignore/any ("blank")\
escape--
comment
- Logical:
and
,or
- Equality:
=
,≠
- Relational:
<
,>
,≤
,≥
- Arithmetic:
+
,-
,×
,/
- Access:
.
A binary operator results in a signal to the left-hand side with one argument, the right-hand side. A set of symbols are reserved for future operators.
A cell is defined with the {}
literal:
cell: {
-- expressions
}
Expressions are messages sent to cells. To send a message:
cell
message with a (slot)
A message is a sequence of Unicode words that may contain slots (arguments). The message forms a signature that the receiving cell's receptors are matched against. Slots are evaluated and attached to the message before it is sent. Literals may be used verbatim, without parenthesis.
An expression ends when a flow operator, binary operator, matching right parenthesis, end of line or comment is encountered.
For example, to log to the console:
console log "hello, world"
This sends a log "hello, world"
message to the console
cell, matching its log (value)
receptor, writing the value to the console's output.
Assignment is done by (implicitly) sending a message to the current cell, self
:
answer: 42
-- is really
self answer: 42
This defines an answer
field with a value of 42
on the current cell.
A field may be defined as mutable by appending a *
:
active: false *
-- mutate its value
active set true
This creates a reference type containing the specified value, similar to Clojure's atoms.
A method is defined as a message signature ''
tied ->
to a cell {}
. The method's cell may have its own fields (local state), and may return a value by assigning to its return
field:
greet: '(name)' -> {
greeting: "Hey, {name}!"
return: greeting
}
-- calling the method (sending a message, "Joe", to the greet method)
greet "Joe" -- "Hey, Joe!"
An inline method implicitly returns the result of its expression. Here's the above method as a one-liner:
greet: '(name)' -> "Hey, {name}!"
Fields are lexically scoped. A method is available within the cell it's defined in and any nested cells:
greet: '(name)' -> "Hey, {name}!"
nested: {
cell: {
greet "Joe" -- "Hey, Joe!"
}
}
A receptor can be thought of as a method defined directly on a cell, not assigned to any field. Here's the greet
method as a receptor on a cell named host
:
host: {
'greet (name)' -> "Hey, {name}!" -- the receptor
}
-- sending the message 'greet "Joe"' to the host cell
host greet "Joe" -- "Hey, Joe!"
Methods can also be passed as values (lambdas) in slots. Because methods have closure, they can emulate control flow statement blocks of traditional languages. Here is the equivalent of an if-then-else
statement using methods without arguments serving as block statements:
marvin: ParanoidAndroid {}
answer = 42
if true -> {
marvin shrug
marvin say "Only if you count in base 13"
}
else -> marvin despair
Having higher precedence, the binary message = 42
is first sent to answer
, resulting in a boolean (true
), which is then sent the if (condition) (true-block) else (false-block)
message. The message is split over several lines (indented) to improve readability. Inline method literals are passed in the true-block
and false-block
slots, to be evaluated by the receptor. Because methods have closure, this effectively emulates block statements in imperative languages.
Expressions are evaluated left-to-right, with binary operators having higher precedence than regular messages. To ensure correct order of evaluation, improve readability, or to use an expression in a slot, wrap the code in ()
:
guess: 3 × (7 + 7)
console log ((guess = answer) "Correct" if true else "You are mistaken")
Nested parentheses can become tedious. To improve readability and prevent parenthitis (also known as LISP syndrome), use of the flow operators pipeline (|
) and compose («
or »
) is prescribed:
console log « guess = answer | "Correct" if true else "You are mistaken"
-- Comparison
a | b | c = ((a) b) c
a « b « c = a (b (c))
a » b » c = c (b (a))
The pipeline operator is suitable for chaining messages (fluent interface):
10 double | negate | print -- sugar
((10 double) negate) print -- desugared
While the compose operators are suitable for a more functional style:
count » increment » console log -- sugar
console log « increment « count -- sugar (equivalent)
console log (increment (count)) -- desugared
The two styles can be combined (the compose operators having higher precedence):
10 double | negate » console log -- sugar
console log « 10 double | negate -- sugar (equivalent)
console log ((10 double) negate) -- desugared
The language offers a small set of easy to understand concepts with a simple syntax, yet should be capable of implementing most constructs typically found in high-level programming languages, while remaining truly multi-paradigm.
Like what you see? Got any ideas or constructive criticism? Feel free to open an issue. This language is still in its early stages of design. Contributions are always welcome, this was not intended to be a solo project :)
From the land of Simula