Continual improvement

This commit is contained in:
Scott Richmond 2023-12-14 19:55:33 -05:00
parent 541b6a51aa
commit feb02dc1b6
2 changed files with 24 additions and 22 deletions

View File

@ -38,7 +38,7 @@ Ludus has a few different types of collections, in increasing order of complexit
In all collection literals, members are written with a separator between them. On the same line, use a comma; or a newline will also separate elements. You may use as many separators as you wish at any point inside a collection or pattern. `(,,,,,,,3,,4,,,,,,)` and `(3, 4)` are the same value.
### Tuples
Tuples are fully-imutable, ordered collections of any kinds of values, delimited by parentheses, e.g. `(1, :a, "foo")`. At current, they have no length limit (although they eventually will). Unlike in some languages, tuples can be empty or contain a single element: `()` and `(:foo)` are both just fine. Tuples largely cannot be manipulated functionally; they must be written as literals and unpacked using pattern matching. They can, however, be converted to lists, either through pattern matching or the `list` function. Their type is `:tuple`.
Tuples are fully-immutable, ordered collections of any kinds of values, delimited by parentheses, e.g. `(1, :a, "foo")`. At current, they have no length limit (although they eventually will). Unlike in some languages, tuples can be empty or contain a single element: `()` and `(:foo)` are both just fine. Tuples largely cannot be manipulated functionally; they must be written as literals and unpacked using pattern matching. They can, however, be converted to lists, either through pattern matching or the `list` function. Their type is `:tuple`.
### Lists
Lists are persistent and immutable ordered collections of any kinds of values, delimited by square braces, e.g. `[1, :a, "foo"]`. They are currently implemented using Clojure's persistent vectors. Their type is `:list`.
@ -49,7 +49,7 @@ Lists may be combined using splats, written with ellipses, e.g., `[...foo, ...ba
Sets are persistent and immutable unordered collections of any kinds of values, which can only contain one instance of any given value. They are written similarly to ordered collections: `${1, :a, "foo"}`. Their type is `:set`.
### Dictionaries, or dicts
Dicts are persistent and immutable associative collections of any kinds of values. Dicts use keywords as keys (and cannot use any other kind of Ludus value as a key, not even strings), but can store any values. Dict literals are written as keyword-value pairs: `#{:a 1, :b false}`. Single words may be used as a shorthand for a key-value pair. Accessing a key that holds no value returns `nil`. Their type is `:dict`.
Dicts are persistent and immutable associative collections of any kinds of values. Dicts use keywords as keys (and cannot use any other kind of Ludus value as a key, not even strings), but can store any values. Dict literals are written as keyword-value pairs: `#{:a 1, :b false}`. Single words may be used as a shorthand for a key-value pair. Accessing a key that holds no value returns `nil`. Their type is `:dict`.
### Namespaces
Namespaces are immutable collections of bindings. They may only be described at the top level of a script. Accessing a key that has no value on a namepsace results in a panic. Their type is `:ns`.
@ -70,7 +70,7 @@ Ludus names are bound permanently and immutably. How do you add something to a l
Ludus provides functions that allow working with persistent collections. They're detailed in [the Prelude](/prelude.md). That said, all functions that modify collections take a collection and produce the modified collection _as a return value_, without changing the original collection. E.g., `append ([1, 2, 3], 4)` will produce `[1, 2, 3, 4]`. (For dicts, the equivalent is `assoc`.)
## Expressions
Ludus is an expression-based language: all forms in the language are expressions and return values. That said, not all expressions may be used everywhere.
Ludus is an expression-based language: all forms in the language are expressions and return values, except `panic!`. That said, not all expressions may be used everywhere.
### Toplevel expressions
Some expressions may only be used in the "top level" of a script. Because they are the toplevel, they are assured to be statically knowable. These include: `ns`, `use`, `import`, and `test`.
@ -150,7 +150,7 @@ Dict patterns may also use a splat as their last member: `let #{:a 1, ...b} = #{
`let` bindings are the basic form of matching and binding in Ludus. It is written `let {pattern} = {non-binding expression}`. The pattern can be arbitrarily complex. If the left-hand side of a `let` binding does not match, Ludus will raise a panic, halting evaluation of the script.
## Scope and blocks
Ludus is lexically scoped. Bindings are valid for the remainder of the scope they act on. To introduce a new scope, Ludus uses a block, a collection of expressions delimited by curly braces and separated by semicolons or newlines. The value of a block, as well as the value of a script, is the last expression in it. In `let foo = {:this; :set; :of; :expressions; "is actually"; :ignored }`, `foo` will be bound to `ignored`.
Ludus is lexically scoped. Bindings are valid for the remainder of the scope they act on. To introduce a new scope, Ludus uses a block, a collection of expressions delimited by curly braces and separated by semicolons or newlines. The value of a block, as well as the value of a script, is the last expression in it. In `let foo = {:this; :set; :of; :expressions; "is actually"; :ignored }`, `foo` will be bound to `:ignored`.
That said, you can use bindings in blocks, which will not be available outside that block--but blocks can use bidnings from their parent scope:
@ -245,7 +245,7 @@ An anonymous lambda is written `fn {tuple pattern} -> {expression}`, `fn (x, y)
A named function is identical to a lambda, with the one change that a word follows the `fn` reserved word: `fn {name} {tuple pattern} -> {expression}`. E.g., `fn add_1 (x) -> add (x, 1)`. The name of the function is bound for the remainder of the scope.
#### Compound functions
Compound functions are functions that have multiple clauses. They must be named, and in place of a single clause, they consist in multiple clauses, separated by semicolons or newlines, delimited by curly braces. Optionally, compound functions may have a docstring, which explains the function's purpose and use, before any of the function clauses.
Compound functions are functions that have multiple clauses. They must be named, and in place of a single clause after a name, they consist in one or more clauses, separated by semicolons or newlines, delimited by curly braces. Optionally, compound functions may have a docstring as their first element after the opening curly brace. The docstring explains the function's purpose and use, before any of the function clauses.
An exampele from Ludus's Prelude:
@ -271,13 +271,14 @@ Unary functions and called keywords may _not_ be partially applied: it is redund
Because of "partial application," Ludus has a concept of an "argument tuple" (which may include a single placeholder) in addition to a tuple literal (which may not include a placeholder).
### Function pipelines, or `do` forms
In place of nesting function calls inside other function calls, Ludus allows for a more streamlined version of function application: the `do` form or function pipeline:
In place of nesting function calls inside other function calls, Ludus allows for a more streamlined version of function application: the `do` form or function pipeline. `do` is followed by an initial expression. `do` expressions use `>` as an operator: whatever is on the left hand side of the `>` is passed in as a single argument to whatever is on its right hand side. For example:
```
let silly_result = do 23
> mult (_, 2) > add (1, _)
> sub (_, 2) > div (_, 9) & silly_result is 5
```
Newlines may appear before any instance of `>` in a `do` expression.
### Called keywords
Keywords may be called as functions, in which case they extract the value stored at that key in the value passed in:
@ -316,11 +317,13 @@ repeat 4 {
Note that `repeat` does two interesting things:
1. It never returns a value other than `nil`. If it's in the block, it disappears.
2. Unlike everything else in Ludus, it requires a block. You cannot write `repeat 4 forward (100)`. (Watch this space.)
2. Unlike everything else in Ludus, it requires a block. You cannot write `repeat 4 forward! (100)`. (But: watch this space.)
### `loop`/`recur`
`loop` and `recur` are largely identical to recursive functions for repetition, but use a special form to allow an anonymous construction and a few guard rails.
The syntax here is `loop <tuple> with { <function clauses> }`. (Or, you can have a single function clause instead of a set of clauses.) The tuple is passed in as the first set of arguments.
```
let xs = [1, 2, 3, 4]
loop (xs, 0) with {
@ -329,8 +332,6 @@ loop (xs, 0) with {
} &=> 10
```
It is `loop <tuple> with { <function clauses> }`. (Or, you can have a single function clause instead of a set of clauses.)
`recur` is the recursive call. It must be in tail position--`recur` must be the root of a synthetic expression, in return position. (At present, this is not checked. It will be statically verified, eventually.)
`recur` calls return to the nearest `loop`. Nested `loop`s are probably a bad idea and should be avoided when possible.
@ -345,17 +346,7 @@ The "toplevel" of a script are the expressions that are not embedded in other ex
`use` loads the contents of a namespace into a script's context. To ensure that this is statically checkable, this must be at the toplevel.
### `ns`
A namespace is an associative data structure that has some restrictions on it beyond dicts, which they resemble. `ns`es must be declared at the toplevel, to ensure static checkability. At current, accessing an absent member on an `ns` leads to a panic; eventually, it will be checked statically at compile-time. `ns`es must be named:
```
ns foo {
bar
baz
:quux 42
}
```
Typically, namespaces are the last thing exported in a script; an `import` then imports a namespace.
Namespaces, `ns`es, may only be described at the toplevel of a script. This is to ensure they can be statically evaluatable.
### `test`
A `test` expression (currently not working!--it will blow up your script, although it parses properly) is a way of ensuring things behave the way you want them to. Run the script in test mode (how?--that doesn't exist yet), and these are evaluated. If the expression under `test` returns a truthy value, you're all good! If the expression under `test` returns a falsy value or raises a panic, then Ludus will report which test(s) failed.
@ -377,6 +368,8 @@ Formally: `test <string> <expression>`.
## Changing things: `ref`s
Ludus does not let you re-bind names. It does, however, have a type that allows for changing values over time: `ref` (short for reference--they are references to values). `ref`s are straightforward, but do require a bit more overhead than `let` bindings. The idea is that Ludus makes it obvious where mutable state is in a program, as well as where that mutable state may change. It does so elegantly, but with some guardrails that may take a little getting used to.
The type of a `ref` is, predictably, `:ref`.
```
ref foo = 42 & foo is now bound to a _ref that contains 42_
add (1, foo) & panic! no match: foo is _not_ a number
@ -386,7 +379,7 @@ value_of (foo) &=> 23; use value_of to get the value contained in a ref
```
### Ending with a bang!
Ludus has a strong naming convention that functions that change state or could cause a panic end in an exclamation point (or, in computer nerd parlance, a "bang"). So anything function that mutates the value held in a reference ends with a bang: `make!` and `update!` take bangs; `value_of` does not.
Ludus has a strong naming convention that functions that change state or could cause an explicit panic end in an exclamation point (or, in computer nerd parlance, a "bang"). So anything function that mutates the value held in a reference ends with a bang: `make!` and `update!` take bangs; `value_of` does not.
This convention also includes anything that prints to the console: `print!`, `report!`, `doc!`, `update!`, `make!`, etc.
@ -404,7 +397,15 @@ A special function, `panic!`, halts script execution with any arguments output a
In fact, the only functions in the Prelude which can cause panics are, at current, `div`, `unwrap!`, and `assert!`. And, of course, `panic!` itself.
### `nil`s, not errors
Ludus, however, tries to return `nil` instead of panicking a lot of the time. So, for example, attempting to get access a value at a keyword off a number, while nonsensical, will return `nil` rather than panicking:
```
let a = true
a :b :c :d :e &=> nil
```
### Result tuples
Instead of exceptions or special error values, recoverable errors in Ludus are handled instead by result tuples: `(:ok, value)` and `(:err, msg)`. So, for example, `unwrap!` takes a result tuple and either returns the value in the `:ok` case, or panics in the `:err` case.
Operations that could fail--especially when you want some information about why--don't always return `nil` on failures. Instead of exceptions or special error values, recoverable errors in Ludus are handled instead by result tuples: `(:ok, value)` and `(:err, msg)`. So, for example, `unwrap!` takes a result tuple and either returns the value in the `:ok` case, or panics in the `:err` case.
Variants of some functions that may have undesirably inexplicit behaviour are written as `{name}/safe`. So, for example, you can get a variant of `div` that returns a result tuple in `div/safe`, which returns `(:ok, result)` when everything's good; and `(:err, "division by zero")` when the divisor is 0. Or, `get/safe` will give you a result rather than returning `nil`. (Althougn `nil` punning makes this mostly unncessary. Mostly.)

View File

@ -85,6 +85,7 @@ fn at {
gte? (n, count (xs)) -> nil
else -> base :nth (xs, inc (n))
}
(_) -> nil
}
fn first {