the programming language
This document is a work-in-progress, outlining what I'd like this language to be. It'll be updated and expanded over time.
Parts of it may be out of date. There will probably be inconsistencies.
Multiple dispatch, dynamic, strongly typed, functional, immutable, message-passing concurrency, prototyping
Scheme, Slate, Self, Io, Haskell, Ruby, Erlang
Integer | 1 |
List | [1, 2] |
Char | 'a' |
String | "foo" |
Double | 1.0 |
Particle | @foo (+ more) |
Block | { a b | a + b } |
1 negate
=> -1
1 negate negate
=> 1
1 + 1
=> 2
'a' as: Integer
=> 97 (ASCII code)
Note that operators must be surrounded by whitespace. This lets us use more symbols in identifiers, e.g. up-to.
Multiple keywords:
0 up-to: 10 do: { n | print n }
This is dispatched as one message, up-to:do:, with the values 0, 10, and { n | print n }
Chained keywords:
"foo/bar" (split-on: '/') (each: print)
=> foo
bar
Currying:
[1, 2, 3] map: (+ 2)
['a', 'b', 'c'] map: (as: Integer)
Simple assignment:
a = 1
This actually defines a method on the current scope which responds to the symbol a being sent by returning 1.
Pattern matching:
0 fact = 1
(n: Integer) fact = n * (n - 1) fact
These are two distinct methods that match on different messages being sent. While the first definiton matches only on the integer 0, the second matches on any Integer. The most precise match gets chosen for the dispatch.
Note that the second definition could also have been simply n fact = n * (n - 1) fact, but explicitly targeting Integer is better form.
Also note that, were Integers and Ints to have the same syntax, the first definiton would be ambiguous: is it on an Integer or an Int? Thus, the literal for Ints was born: 1i, mirroring the distinction between Float and Double literals (2.0f vs. 2.0).
Something more fancy:
(n: Integer) up-to: (end: Integer) do: action =
if: (n == end)
then: []
else: (action n . ((n + 1) up-to: end do: action))
New primitive forms of data can be defined with data.
For example:
data: Bool delegates: { True; False }
This defines the following constructor hierarchy:
Bool
/ \
True False
Note that Bool is a valid constructor for itself.
The following is legal:
0 as: Bool = False
1 as: Bool = True
Constructors can have additional slots:
data: Result delegates: { Ok value; Error }
foo = Ok 1
foo value
=> 1
These can also be pattern matched:
(Ok (value = Int)) bar = "this OK is an int! " .. v as: String
(Ok (value = String)) bar = "this OK is a string! " .. v
Note that in pattern matches, the order and existence of the slots being matched on doesn't matter.
The language uses message-sending to objects, using multiple dispatch.
Defining "methods" on objects is really just defining patterns for objects to respond to.
For example, defining this nonsensical method which takes mutiple keywords:
(n: Integer) add: (d: Double) mul: (f: Float) =
(n + d) * f
Really defines a method that responds to an add:mul: message sent along with 3 values (any Integer, any Double, and any Float).
Internally, the "matching" is done simply using pattern matching - the same kind of pattern matching nested inside of the message to match on n, d, and f.
This means that you can match on actual values being sent as messages to the object, as well:
Integer "foo" = "Integer responding to foo!"
1 "foo"
=> "Integer responding to foo!"
Combined with pattern matching, this can yield something akin to defining methods on individual integers:
1 foo = "fooing on one"
2 foo = "fooing on two"
(n: Integer) foo = "fooing on any old number"
Or, for that matter, any old message:
Integer _ = "unknown message" print
This will respond to any value being sent to any Integer by printing "unknown message". It will not override other messages as matches are sorted by precision.
Scoping ======= The current scope is just a prototype, delegating to one higher up on the chain, i.e. lexical scoping.
The toplevel target can be replaced. Defining variables is really just adding methods that the current scope responds to to yield the value:
foo = 1
foo
=> 1
bar: n = n + 2
bar: 3
=> 5
As blocks are introduced, usual lexical scoping rules apply; inner blocks can access outer values that were defined before the block's execution, but outer blocks cannot see values defined in the inner blocks.
The provides message-passing concurrency very similar to Erlang's model. The goal is to see what kind of overlap there can be with messages sent to/from processes and messages sent to/from objects (as is already inherent in the language).
Brainstorming:
pid = {
{
"waiting for message..." print;
receive print;
"got message!" print
} repeat
} spawn
=> waiting for message...
pid do-something
=> :do-something
got message!
waiting for message...
pid do-something-with: 0
=> do-something-with: 0
got message!
waiting for message...
The value of using the native message-passing system will shine more with an Actor model; with that you'll get full multiple-dispatch and pattern-matching, just like normal message-passing.