Skip to content

joakim/kay

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kay

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

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

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.


Messages

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).


Data types

Reference types

  • {}   – cell
  • nothing – bottom value (a cell that only ever returns itself)

Value types

  • booleantrue or false
  • 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.


Values

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.


Syntax

Literals

  • {}  cell
  • []  collection
  • 42  number
  • ""  string
  • ''  message definition
  • ()  message parameter, expression
  • ->  method
  • true
  • false
  • nothing

Operators

Flow

  • |   pipeline
  • »   compose left-to-right
  • «   compose right-to-left
  • ,   expression separator

Various

  • *   mutable
  • _   ignore/any ("blank")
  • \   escape
  • -- comment

Binary

  • 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.


Examples

Cells and messaging

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.

Fields

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.

Methods and receptors

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.

Evaluation operators

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
ab | 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

What it brings

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.


Contributions

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 :)

Ideas...

More examples...


From the land of Simula