459 lines
20 KiB
Markdown
459 lines
20 KiB
Markdown
|
# Ludus for programmers
|
||
|
## A brief introduction
|
||
|
|
||
|
Ludus is mostly understood by its research and design team as a language for _learners_.
|
||
|
It is a _pedagogical_ language, whose primary purpose is to lead students to critical encounters with the history and present of computing.
|
||
|
The design principles, then, lead with learnability as well as making certain key examples in the history of computing easy.
|
||
|
|
||
|
Because of that, Ludus has some weird features.
|
||
|
It will likely not feel especially familiar, particularly if you have not written funtional code before.
|
||
|
We encourage you to feel disoriented by it, and to lean into that disorientation.
|
||
|
Instead of trying to write code like you have in the past, write code like Ludus wants you to.
|
||
|
|
||
|
There are two big influences on Ludus.
|
||
|
In terms of historical languages, Ludus draws a lot from Logo and Scheme.
|
||
|
In terms of contemporary languages, Ludus has deep affinities with Elixir and Clojure.
|
||
|
To put it in an abstraction cluster, Ludus is a dynamically typed, extremely strict functional programming language with persistent data structures, deep immutability, and pattern matching.
|
||
|
None of these are especially mainstream.
|
||
|
|
||
|
It is not "batteries included," but rather offers a quite minimalistic set of capabilities.
|
||
|
These are devised, as I noted above, to make encountering key concepts from the history of comptuing easy.
|
||
|
But beyond that, Ludus is extremely minimal, in the tradition of Scheme and Logo.
|
||
|
The profound pedagogical perspective behind Scheme and Logo is that building the things you want is an important motivator for learning how to make computers do things.
|
||
|
Ludus follows in this path.
|
||
|
|
||
|
If you've mostly written object-oriented code, Ludus will, frankly, feel weird.
|
||
|
And that's awesome.
|
||
|
|
||
|
### Ludus is expression based
|
||
|
Ludus has no statements, only expressions.
|
||
|
Every expression returns a value, including conditional forms like `if`, `when`, and `match`.
|
||
|
|
||
|
In Ludus, different types of expressions are called _forms_, riffing on the grand Lisp tradition.
|
||
|
|
||
|
### Ludus is dynamically typed
|
||
|
Like its inspirations, Elixir and Clojure and the whole family of Lisps, Ludus is dynamically typed.
|
||
|
It is _strictly_ typed, however.
|
||
|
Unlike Javascript, Ludus will never convert between values of one type or another.
|
||
|
Ludus has the following types:
|
||
|
* `:nil`: The type of `nil`, Ludus's name for nothing.
|
||
|
* `:bool`: Boolean--`true` or `false`.
|
||
|
* `:number`: IEEE-754 64-bit floating point numbers. Ludus does not have an integer type. That said, Ludus avoids `NaN` as much as possible.
|
||
|
* `:string`: UTF-8 strings.
|
||
|
* `:keyword`: Keywords are self-identical atoms, evaluating only to themselves. The equivalent of a `Symbol` in Javascript (or a keyword in Clojure or Elixir). (The types in this list--and in Ludus--are represented as keywords.)
|
||
|
* `:tuple`: Fixed-length, fully immutable collections of zero or more values. Tuples are comma-or-newline separated values, surrounded by parentheses: `(1, 2, 3)`.
|
||
|
* `:list`: Persistent, immutable ordered list of any number of Ludus values. Lists are comma-or-newline separated values, surrounded by square brackets: `[:foo, :bar, :baz]`.
|
||
|
* `:dict`: Persistent, immutable associative collection of keyword keys and any Ludus values. Dicts are comma-or-newline separated keyword-and-value pairs, introduced by `#{` and closed with a curly brace: `#{:a 1, :b 2}`.
|
||
|
* `:fn`: Functions!
|
||
|
* `:box`: A holder for any value, which can change over time. A cognate of Clojure's atom. This is the only place in Ludus you will find mutable state.
|
||
|
|
||
|
At current, three other types are planned but not implemented: `:set`, `:pkg`, `:process`.
|
||
|
|
||
|
Ludus does not allow creating new types.
|
||
|
|
||
|
### Ludus has a weird comment character
|
||
|
It uses the ampersand--`&`--to introduce comments.
|
||
|
It does not have mulitline comments.
|
||
|
|
||
|
### Ludus does not have variables, it has bindings
|
||
|
The basic form of assignment in Ludus looks very familiar:
|
||
|
|
||
|
```
|
||
|
let foo = 42
|
||
|
let bar = :quux
|
||
|
let baz = "hello, world"
|
||
|
```
|
||
|
|
||
|
These are _let bindings_.
|
||
|
|
||
|
#### Let bindings are extremely immutable
|
||
|
They may not change.
|
||
|
In addition, you may not shadow let bindings.
|
||
|
You may not shadow a binding, like in Rust, where you can re-use the name and discard the old binding.
|
||
|
You may also not bind the same name in a nested scope (e.g., inside a function).
|
||
|
Once you bind a name, it is forever bound to that value.
|
||
|
The value in this is that language learners need (almost) never wonder what value a name is bound to, since it can never change.
|
||
|
|
||
|
Except, of course, with function calls.
|
||
|
|
||
|
#### The left-hand side of a let binding is a _pattern_
|
||
|
Ludus makes extensive use of pattern matching.
|
||
|
The left-hand side of a let binding need not be a simple name.
|
||
|
A simple name is only one kind of pattern.
|
||
|
|
||
|
For example, this is valid Ludus:
|
||
|
```
|
||
|
let foo = 42
|
||
|
let 42 = foo
|
||
|
let nil = nil
|
||
|
```
|
||
|
The second line does nothing _except_ match the value on the left hand side to the value on the right hand side.
|
||
|
If a let binding does not match, e.g., `let 1 = 2`, then Ludus will panic.
|
||
|
|
||
|
Patterns can also be used to destructure all Ludus collections:
|
||
|
```
|
||
|
let (:ok, x) = (:ok, 42) & tuple pattern: x is now 42
|
||
|
let [l, m, ...] = [1, 2, 3] & list pattern: l = 1, m = 2
|
||
|
let #{a, b} = #{:a 1, :b 2} & dict pattern: a = 1, b = 2
|
||
|
```
|
||
|
#### Collection patterns are exact & complete, unless otherwise specified
|
||
|
In the second line in the example above, the pattern `[l, m, ...]` includes a splat pattern (or splattern).
|
||
|
If we had written `let [l, m] = [1, 2, 3]`, Ludus would have panicked with `no match`.
|
||
|
There are three list members on the right, only two on the left.
|
||
|
The splat, `...` (or ellipsis) matches "anything else in the list."
|
||
|
You may also include a name after the splat, which will be bound to "anything else in the list," e.g.
|
||
|
```
|
||
|
let [head, ...tail] = [1, 2, 3, 4, 5]
|
||
|
head &=> 1
|
||
|
tail &=> [2, 3, 4, 5]
|
||
|
```
|
||
|
|
||
|
#### The placeholder is a special pattern
|
||
|
A placholder pattern, `_`, matches against anything but does not bind a name.
|
||
|
Also, you may name your placholders, e.g., `_ignored`, but that is for the programmer only.
|
||
|
Named or unnamed placeholder patterns are strictly equivalent.
|
||
|
|
||
|
### Ludus panics
|
||
|
Ludus has exactly one type of runtime error: a panic.
|
||
|
Panics will always crash the program.
|
||
|
You cannot `catch` a panic.
|
||
|
|
||
|
You can raise a panic thusly:
|
||
|
```
|
||
|
panic! "oh shit"
|
||
|
```
|
||
|
`panic!` may only take a single value, but that value can be a collection.
|
||
|
|
||
|
**Eventually** (not long from now!), Ludus will have actor-style concurrency, and a panic will only bring down a process.
|
||
|
But this is not yet implemented.
|
||
|
|
||
|
### Almost everything is a function
|
||
|
Ludus does not have operators.
|
||
|
In the grand Lisp tradition, all operations look like (and, for the most part, substantively are) function calls.
|
||
|
|
||
|
* To add two numbers in Ludus: `add (1, 2) &=> 3`.
|
||
|
* To subtract one number from another: `sub (2, 1) &=> 1`
|
||
|
* To determine, are two things equal?: `eq? (3, "three") &=> false`
|
||
|
|
||
|
### The Prelude is loaded before every evaluation
|
||
|
The Ludus Prelude is its standard library, and all functions in the Prelude are available in every Ludus script.
|
||
|
Consult the [Prelude documentation](./prelude.md) for information for all functions in Prelude.
|
||
|
Everything you'll want to do with Ludus involves the Prelude in some way.
|
||
|
|
||
|
Note that most Prelude function names can, in fact, be shadowed by local bindings in a script.
|
||
|
That said, there are several functions that, for optimization reasons, are "builtin," whose names may never be used, e.g., `add`, `sub`, `eq?`, `inc`, `dec`, and so on.
|
||
|
|
||
|
#### Boolean functions are "special forms"
|
||
|
`and` and `or` are special, in that they are compiled differently than other functions.
|
||
|
Their arguments are evaluated lazily, rather than eagerly, so they can short-circuit (and prevent panics).
|
||
|
|
||
|
### Ludus lists and dicts are persistent
|
||
|
Dicts and lists are persistent.
|
||
|
This means you cannot mutate them.
|
||
|
However, you can still add things to a list--you just get back a _new_ list with the value added:
|
||
|
```
|
||
|
let foo = [1, 2, 3]
|
||
|
let bar = append (foo, 4) &=> [1, 2, 3, 4]
|
||
|
|
||
|
let baz = #{:a 1, :b 2}
|
||
|
let quux = assoc (baz, :c, 3) &=> #{:a 1, :b 2, :c 3}
|
||
|
|
||
|
foo &=> [1, 2, 3]
|
||
|
baz &=> #{:a 1, :b 2}
|
||
|
```
|
||
|
Persistent data structures are wonderful, and use a lot of computer science magic to make them competitive in terms of performance: they use "structural sharing" and attempt "opportunistic mutation."
|
||
|
|
||
|
### Ludus has three conditional forms
|
||
|
The three conditional forms in Ludus are `if`, `when`, and `match`.
|
||
|
|
||
|
#### The `if` form
|
||
|
Ludus's base conditional form is `if`:
|
||
|
```
|
||
|
if foo then bar else baz
|
||
|
```
|
||
|
Does what you'd expect!
|
||
|
But with two caveats.
|
||
|
|
||
|
(Before the caveats: you can put newlines before `then` and `else`.)
|
||
|
|
||
|
#### Falsy falues: `nil` and `false`
|
||
|
The condition (`foo` in the example above) is evaluated not strictly as `true` or `false`.
|
||
|
Ludus "falsy" values are `nil` and `false`.
|
||
|
Everything else is truthy, including `0` and `()` (the empty tuple), and `""` (the empty string).
|
||
|
This holds across anywhere in the language you are dealing with notions of truth and falsity: `if` and `when` forms, `guard` expressions in `match` forms, `and` and `or`, etc.
|
||
|
|
||
|
#### Both `then` and `else` are obligatory
|
||
|
`if` forms in Ludus _must_ have both `then` and `else` branches.
|
||
|
This is because every expression in Ludus must return a value.
|
||
|
If you want to throw away a value, you can do that, but you'll need something like `else nil` or `else :nothing`.
|
||
|
|
||
|
#### The `when` form
|
||
|
If you have multiple conditions you'd like to chain together, `when` forms are what you want.
|
||
|
(Ludus does not have an `else if` form.)
|
||
|
`when` puts multiple clauses together, each of which has a left-hand condition expression and a right-hand body expression: `<condition expr> -> <body expr>`
|
||
|
Ludus will evaluate the left-hand expression, and, if it's truthy, evaluate and return the corresponding right-hand expression:
|
||
|
```
|
||
|
when {
|
||
|
eq? (1, 2) -> :nope
|
||
|
eq? (3, 4) -> :not_this_either
|
||
|
eq? (0, 0) -> :this!
|
||
|
} &=> :this!
|
||
|
```
|
||
|
If no clause in a when form has a truthy left-hand side, Ludus panics.
|
||
|
|
||
|
Any truthy value will do if you want the equivalent of an `else` branch.
|
||
|
By convention, `:else` is used as the catch-all at the end of a match form.
|
||
|
|
||
|
#### The `match` form
|
||
|
A `match` form is the equivalent of a `switch` statement in C-family languages.
|
||
|
It is much more powerful, however.
|
||
|
`match` is much beloved by functional programmers.
|
||
|
`match` forms are similar to `when` forms, but they require a value--a "scrutinee."
|
||
|
And, in place of expressions on the left-hand side of their clauses, they have patterns: `<pattern> -> <expr>`.
|
||
|
They attempt to match the value against each pattern until there is a match.
|
||
|
If no clause matches, then Ludus panics.
|
||
|
|
||
|
This is an extremely common pattern in Ludus:
|
||
|
```
|
||
|
let might_fail = (:ok, 42)
|
||
|
match might_fail with {
|
||
|
(:ok, value) -> print! ("got {value}!")
|
||
|
(:err, _) -> print! ("the thing failed")
|
||
|
} &=> :ok, prints "got 42!"
|
||
|
```
|
||
|
|
||
|
##### Match clauses may have a guard expression
|
||
|
A match clause may also have a guard expression.
|
||
|
Afer the pattern and before the arrow, you may put `if <expr>`.
|
||
|
(Here you may not use `then` or `else`.)
|
||
|
Bindings made in the pattern are valid in that expression.
|
||
|
If the expression is truthy, then that's a match.
|
||
|
If it's falsy, no match:
|
||
|
```
|
||
|
let foo = 42
|
||
|
let bar = 23
|
||
|
match bar with {
|
||
|
x if even? (x) -> :even
|
||
|
x if eq? (foo, x) -> :foo
|
||
|
_ -> :odd_not_foo
|
||
|
} &=> :odd_not_foo
|
||
|
```
|
||
|
|
||
|
### Ludus groups expressions together with blocks
|
||
|
A block groups expressions together.
|
||
|
Ludus blocks must have at least one expression (because everything in Ludus must return a value).
|
||
|
A block evaluates to its last expression.
|
||
|
Expressions are separated by one or more terminators--newlines or semicolons.
|
||
|
Use curly braces to form a block:
|
||
|
```
|
||
|
if true
|
||
|
then {
|
||
|
:first; :second & these are two different expressions
|
||
|
:third
|
||
|
}
|
||
|
else {
|
||
|
:nothing
|
||
|
} &=> :third
|
||
|
```
|
||
|
Blocks can go most anywhere expressions can go.
|
||
|
|
||
|
### Ludus has synthetic expressions
|
||
|
We have already seen function calls, e.g., `add (1, 2)`.
|
||
|
This is a _synthetic_ expression, which is a chained combination of bound names, tuples, and keywords.
|
||
|
The root of a synthetic expression may be either a name or a keyword.
|
||
|
Subsequent terms must either be tuples or keywords.
|
||
|
They are evaluated by applying the second term to the first, then applying the third term to the result of that first application, and applying the fourth to the second result, and so on.
|
||
|
Applying a tuple will call something as a function: `add (1, 2)`.
|
||
|
Applying a keyword will access the value stored at that key in a dict: `foo :bar`.
|
||
|
|
||
|
These may be chained arbitrarily.
|
||
|
Take, for example, `foo :bar (1, 2) :baz`.
|
||
|
This accesses `:bar` on `foo`, applies the arguments `(1, 2)` to that value (presumably a function), and then access `:baz` on value returned by that function.
|
||
|
|
||
|
#### Keywords may be called as functions
|
||
|
Following Clojure's example, you may call a keyword as a function: `foo :bar` and `:bar (foo)` are strictly equivalent.
|
||
|
|
||
|
### Ludus has function pipelines
|
||
|
In addition to normal function application, Ludus also has function pipelines, equivalent to Elixir's pipelines or Clojure's thread macros.
|
||
|
In these, the first term is applied, as a single argument, to the second. The result of that is then applied, as a single argument, to the third, and so on.
|
||
|
|
||
|
Function pipelines are introduced by the reserved word, `do`.
|
||
|
These two expressions are exactly equivalent:
|
||
|
```
|
||
|
do foo > bar >
|
||
|
baz > quux
|
||
|
quux (baz (bar (foo)))
|
||
|
```
|
||
|
Newlines may be inserted _after_ the `>` pipeline symbol, not before.
|
||
|
Note that a line ending with the pipeline symbol will "eat" the line after it, even if separated by many terminators, so be careful.
|
||
|
|
||
|
Because keywords can be called like functions, bare keywords may be used in function pipelines.
|
||
|
|
||
|
### Ludus has partial function application
|
||
|
Any function in Ludus may be partially applied by using the placholder, `_`, in place of an argument.
|
||
|
Doing so returns a function that takes a single argument.
|
||
|
When that function is called, it calls the original function with that argument put in the placeholder's position.
|
||
|
Here's a simple example:
|
||
|
```
|
||
|
let double = mult (2, _)
|
||
|
double (3) &=> 6
|
||
|
double (12) &=> 24
|
||
|
```
|
||
|
Partially applied functions play very nicely with pipelines:
|
||
|
```
|
||
|
let double = mult (2, _)
|
||
|
let mynums = [1, 2, 3, 4, 5, 6]
|
||
|
do mynums >
|
||
|
filter (even?, _) > &-> [2, 4, 6]
|
||
|
map (double, _) &=> [4, 8, 12]
|
||
|
```
|
||
|
|
||
|
### Ludus function definitions
|
||
|
Functions come in three flavours, all of which have a concept of a function clause.
|
||
|
A function clause is a special case of a match clause: it has a _tuple_ pattern on its left hand side (since we call functions with tuples).
|
||
|
Otherwise,
|
||
|
|
||
|
#### Anonymous lambdas
|
||
|
An anonymous lambda is the `fn` reserved word, followed by a function clause:
|
||
|
```
|
||
|
let double = fn (x) -> mult (x, 2)
|
||
|
double &=> fn anon.
|
||
|
double (13) &=> 26
|
||
|
```
|
||
|
|
||
|
#### Named functions
|
||
|
Named functions are exactly the same as anonyomous lambdas, but they have a name between `fn` and the clause:
|
||
|
```
|
||
|
fn double (x) -> mult (x, 2)
|
||
|
double &=> fn double
|
||
|
double (-4) &=> -8
|
||
|
```
|
||
|
|
||
|
#### Compound functions
|
||
|
Compound functions have multiple clauses, separated off by curly braces:
|
||
|
```
|
||
|
fn foo? {
|
||
|
("foo") -> true
|
||
|
(:foo) -> true
|
||
|
(_) -> false
|
||
|
}
|
||
|
foo? (:bar) &=> false
|
||
|
```
|
||
|
There's a very close relationship between match forms and function definitions.
|
||
|
|
||
|
##### docstrings
|
||
|
A compound function may, optionally, take a string before any of its clauses, that serves as documentation for the function:
|
||
|
```
|
||
|
fn foo? {
|
||
|
"Tells if its argument is a `foo`."
|
||
|
("foo") -> true
|
||
|
(:foo) -> true
|
||
|
(_) -> false
|
||
|
}
|
||
|
```
|
||
|
Ludus will print the documentation for a function by means of the `doc!` function.
|
||
|
|
||
|
### Ludus has a convention of "commands": they end with a bang
|
||
|
By convention, Ludus functions that end in an exclamation point have side effects.
|
||
|
These are called _commands_.
|
||
|
`doc!` is a command; so is `print!`.
|
||
|
|
||
|
Ludus commands typically return the keyword `:ok` rather than `nil`.
|
||
|
|
||
|
Much of Ludus involves manipulating turtle graphics commands, `forward!`, and so on.
|
||
|
|
||
|
### Ludus has loops, but you should probably use recursion
|
||
|
Ludus, in the grand Lisp (and Logo) tradition, eschews looping constructs in favour of functional recursion.
|
||
|
Ludus is tail-call optimized, which means that recursion, even mutual recursion, is as fast as looping.
|
||
|
The `loop` form, anyway, isn't anything like you're expecting; it's basically function calls.
|
||
|
|
||
|
Two examples of factorial, looping and recurisve:
|
||
|
```
|
||
|
loop (6, 1) with {
|
||
|
(0, acc) -> acc
|
||
|
(n, acc) -> recur (dec (n), mult (n, acc))
|
||
|
} &=> 720
|
||
|
|
||
|
fn fact {
|
||
|
(n) -> fact (n, 1)
|
||
|
(0, acc) -> acc
|
||
|
(n, acc) -> fact (dec (n), mult (n, acc))
|
||
|
}
|
||
|
|
||
|
fact (6) &=> 720
|
||
|
```
|
||
|
The difference between these is that Ludus will throw a compile error if `recur` isn't in tail position.
|
||
|
In addition, all clauses in a loop form, and all invocations of `recur` must have the same arity, whereas functions may have clauses of arbitrary arity.
|
||
|
|
||
|
### Ludus has multiple "levels" of expressions
|
||
|
Not all Ludus expressions can appear anywhere you need an expression.
|
||
|
Ludus has four levels of expressions that restrict where they may go: simple, nonbinding, expressions, and toplevel.
|
||
|
* _Simple_ expressions include all literals as well as bare names and synthetic expressions. They may go anywhere you expect an expression, e.g. in the condition position in if or when forms. But in these positions, you may not use, say, another conditional form, nor bind a name.
|
||
|
* _Nonbinding_ forms include all expressions _except_ those that bind a name. These include all simple expressions, as well as conditional expressions (`if`, `when`, `match`), anonymous lambdas, and `do` pipelines.
|
||
|
* _Expressions_ (tout court) include all Ludus expressions, including those that bind names: `let`, named `fn`s, and `box`.
|
||
|
* _Toplevel_ expressions may only go at the root scope of a script. At current, the are not yet implemented (`pkg`, `use`, `test`). These are statically checked.
|
||
|
|
||
|
### Ludus has carefully managed state
|
||
|
At some point, you need state.
|
||
|
(You need far less than you think!)
|
||
|
For that, you need a `box`.
|
||
|
A box holds a value that can change over time.
|
||
|
It can hold any other Ludus value, including a box.
|
||
|
Getting a value out of a box isn't as simple, however, as using its name.
|
||
|
The name is bound to the _box_, not its value.
|
||
|
To get the value out of a box, you use the `unbox` function:
|
||
|
```
|
||
|
box foo = 42
|
||
|
foo &=> box [42]
|
||
|
unbox (foo) &=> 42
|
||
|
```
|
||
|
To change the value in a box, you use either the `store!` command, or the `update!` command.
|
||
|
`store!` takes a box and a value, and simply puts the new value in the box.
|
||
|
`update!` (not to be confused with the function `update`) takes a box and a function, and updates the value in the box by applying the function to the value in the box:
|
||
|
```
|
||
|
box foo = 42 &=> box [42]
|
||
|
store! (foo, 23) &=> box [23]
|
||
|
update! (foo, add(13, _)) &=> box [36]
|
||
|
unbox (foo) &=> 36
|
||
|
```
|
||
|
|
||
|
#### Boxes are not variables
|
||
|
We have put the section on boxes last in this introduction because boxes are _not_ variables.
|
||
|
Most state can actually be, and within Ludus, absolutely ought to be, modeled not with boxes but with recursive functions.
|
||
|
|
||
|
Consider the factorial example from earlier.
|
||
|
|
||
|
A straightforward Javascript implementation might look like this:
|
||
|
```javascript
|
||
|
function fact (n) {
|
||
|
let acc = 1;
|
||
|
while n > 1 {
|
||
|
acc = n * acc;
|
||
|
n--;
|
||
|
}
|
||
|
return acc;
|
||
|
}
|
||
|
```
|
||
|
You'll note that the `while` statement doesn't have an easy equivalent in Ludus.
|
||
|
But if you were really stubborn about wanting to twised boxes into variables, you could do something like this:
|
||
|
```
|
||
|
fn fact (n) -> {
|
||
|
box acc = 1
|
||
|
loop (n) with (m) -> if lt? (m, 1)
|
||
|
then unbox (acc)
|
||
|
else {
|
||
|
store! (acc, mult (m, unbox (acc)))
|
||
|
recur (dec (m))
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
Let me tell you, this is _wild_ Ludus.
|
||
|
The `loop` there is very weird indeed.
|
||
|
|
||
|
The short version is, if you can possibly avoid it--and you probably can--don't use boxes.
|
||
|
|
||
|
The more complex version is this:
|
||
|
The functional and immutable nature of Ludus will change your ideas about programming.
|
||
|
This is part of the point.
|
||
|
So
|