This document describes the Lust core language and should familiarize you with what Lust can do without its standard library.
In lust all functions are anonymous. In effect this means that functions have similar semantics to normal pieces of data like strings and numbers. Here's a simple function that takes zero arguments and prints "hello":
(fn () (println 'hello))
Functions
have the form (fn ARG-LIST BODY)
where ARG-LIST
is a list of symbols
and BODY
is an expression to evaluate when the
function is called.
When a function is called two things happen. First, a new
environment is created where each symbol in the
function's ARG-LIST
is bound to its corresponding
value in the function call. Second, the body is executed in the
new environment. The value yielded by that evaluation is
returned from the function.
To call a function place it at the front of a list and then place its arguments afterwards. For example, consider the following Lust REPL session:
lust> ((fn (a b) (add a b)) 1 2)
=> 3
Let's walk through what happened there. First, Lust saw that it
was evaluating a list so it looked in the first position for the
function to call. In the first position it saw (fn (a b)
(add a b))
which it recognized to be a user defined
function. Having determined it was calling that function, it
bound its arguments 1
and 2
to the
symbols a
and b
respectively and then
evaluated the function's body, returning 3.
In Lust functions can also take a variable number of
arguments. We call functions that do this varadic functions. In
order to declare a function that takes a variable number of
arguments we can add an &
symbol before the last
argument in the function. If Lust see's the &
symbol in that position it will then bind all remaining
arguments to the function to the last value in the form of a
list.
Lets take a look at this in action:
lust> ((fn (& vargs) vargs) 1 2)
=> (1 2)
As we can see, because we used the &
symbol, all of
the remaining arguments after binding the preceding ones were
bound to vargs
in the form of a list.
Varadic functions are a very powerful part of Lust and enable
the creation of many very interesting functions. You can take a
look at
the standard library
function +
. In the meantime, here's an
example REPL session to demonstrate some additional varadic
function capabilities:
lust> ((fn (first & rest) first) 1 2)
=> 1
lust> ((fn (first & rest) rest) 1 2)
=> (2)
lust> ((fn (first & rest) rest) 1)
=> ()
In Lust variables are a way of referring to a piece of data. When you evaluate a variable you'll get the piece of data that the variable is referring to. When you declare a variable it will only be accessible in the scope that you declare it to be in. Lust has two ways to chose that scope:
let
function to declare a variable
in the current local scopeset
function to declare a variable
in the current global scopeHere are some examples of variable scoping in Lust:
Local variables can not be accessed outside of the scope that they were created in
lust> (set 'localvar (fn (a) (let local a)))
=> (fn (a) (set (quote local) a))
lust> (localvar 2)
=> 2
lust> local
local
^----
| error: failed to resolve identifier local
*--> repl:0:0
The same can not be said about variables placed in the global scope.
lust> (set 'globalvar (fn (a) (set 'global a)))
=> (fn (a) (set (quote global) a))
lust> (globalvar 42)
=> 42
lust> global
=> 42
In Lust all environments have closures so variables declared in outer scopes can be referenced by inner enviorments.
lust> (let outerlocal 10)
=> 10
lust> (set 'f (fn () outerlocal))
=> (fn () outerlocal)
lust> (f)
=> 10
The Lust core has only one control flow expression, that being
the venerable if
expression. If expressions are
in the form (if COND THEN ELSE)
. Their evaluation
is pretty standard, but if you're interested in the details,
they are evaluated as follows:
COND
expression is evaluatedCOND
did not evaluate to the empty list,
the THEN
expression is evaluated in a new
enviorments with a parent environment equal to the
enviorments in which COND
was evaluated.
COND
did evaluate to the empty list,
the ELSE
expression is evaluated in a new
enviorments and with a parent enviorments equal to the
enviorments in which COND
was evaluated.
THEN
or ELSE
,
depending on which one was chosen.
Here's an example of if expression evaluation:
lust> (if 1 2 3)
=> 2
lust> (if () 2 3)
=> 3
Lust has single line comments and comments are entirely ignored by the evaluator. Comments start with a semicolon and cause the rest of their line to be ignored. Here's an example:
lust> (if 1 2 3) ; this is a comment
=> 2
Everything in Lust is a list. When the evaluator sees a list it expects the first item in the list to be a function and the remaining items to be arguments for that function.
Here are some examples of lists:
(add 1 2)
(fn (a b c) c)
(hello)
()
(() (1 2 3) 'hello (()))
The exception to that rule is the empty list. When the evaluator sees an empty list it evaluates it to another empty list. The empty list is the only value in Lust that is considered to be false. See control flow for an example of this.
Because Lists are such an essential part of Lust, Lust has three
powerful functions to assist in manipulating
them car
, cdr
,
and cons
. If you've used a Lisp language before
these will be very familiar.
car
takes one argument, a
list, and returns the first item in that list. If the list
is empty it just returns the empty list.
lust> (car '(1 2 3))
=> 1
lust> (car '())
=> ()
cdr
takes one argument, a list,
and returns a new list containing all but the last item in
the list.
lust> (cdr ())
=> ()
lust> (cdr '(1 2 3))
=> (2 3)
cons
takes two arguments, an
item and a list and returns a new list consisting of the
first argument prepended to the second.
lust> (cons 1 ())
=> (1)
lust> (cons 1 '(2 3))
=> (1 2 3)
Numbers in Lust probably work exactly the way you expect. All numbers in Lust are represented by 32 bit floating point numbers. You can perform a variety of operations on numbers with the Lust core. Here are some example numbers:
lust> -1.5
=> -1.5
lust> 10
=> 10
lust> 1.5
=> 1.5
lust> 300000000
=> 300000000
lust> -40.6
=> -40.6
The Lust core supports addition, subtraction, multiplication, and division of numbers. Lust then leaves it up to the standard library to extend these operations. Here are some examples of operations on numbers:
lust> (add 1 1)
=> 2
lust> (div 5 2)
=> 2.5
lust> (mul 2 5)
=> 10
lust> (sub 2 1)
=> 1
In Lust strings are syntactic sugar for lists of characters. If you print a list of characters Lust will figure out that you mean to print a string and print a regular string. Here's an example demonstrating that behavior:
lust> (let foo "hello")
=> "hello"
lust> (let foo (cons 1 foo))
=> (1 'h' 'e' 'l' 'l' 'o')
lust> foo
=> (1 'h' 'e' 'l' 'l' 'o')
lust> (let foo "hello")
=> "hello"
lust> (println foo)
"hello"
=> ()
lust> (let foo (cons 1 foo))
=> (1 'h' 'e' 'l' 'l' 'o')
lust> (println foo)
(1 'h' 'e' 'l' 'l' 'o')
=> ()
Notice that before we appended a number to foo
Lust
recognized that foo
was a string and displayed it
as such.
You might notice that despite strings being lists of characters,
you can't directly input characters into Lust. For
example, 'a'
won't parse as a character. This is
not an intentional design decision as much as it is just a
feature that hasn't been implemented yet.
Luckily enough for us though, Lust's flexibility makes it easy enough to write a macro to take a string with one character and convert it to a char. Here it is:
(let char (macro (s)
`(if (eq (len ,s) 1)
(car ,s)
(error "can not convert to char"))))
In Lust, quotation is a way to indicate to the evaluator that an
expression should evaluate to itself. Quotation can be
accomplished via regular function calls and via some nice
syntactic sugar. Let's start with a small example. Notice how
below when foo
is evaluated normally it evaluates
to the value it references, but when it is quoted it just
evaluates to foo
.
lust> (let foo 10)
=> 10
lust> foo
=> 10
lust> (quote foo)
=> foo
In the above example we used the quote
function
which takes an argument, and does nothing to it. Another way to
use the quote function is to prepend a '
symbol to
something. Here's that same example but rewritten to use the
quote special form:
lust> (let foo 10)
=> 10
lust> foo
=> 10
lust> 'foo
=> foo
There are times where quotation isn't quite what we want
though. Sometimes we want to quote part of an expression but not
the rest. For example, say we'd like to write
a importq
function that doesn't evaluate its first
argument so we're not always needing to quote it. A first
attempt might look something like this:
(set 'importq (macro (target value)
'(import (quote target) value)))
This looks good, but sadly does not work. Expanding an invocation of it in the REPL looks like this:
lust> (macroexpand (importq foo))
=> (import (quote target))
What we really want to do here is selectively evaluate parts of
this list. Specifically, we'd like to evaluate target and value
so that the macro expands to the code we expect. Quote alone
won't do that, but lucky for us, Lust has
the quaziquote
function.
quaziquote
allows us to quote most of a
list. When a list is quaziquoted it everything inside will be
evaluated except calls to a special comma
function
which behaves the same as eval
when inside a
quaziquote and runs while nothing else
does. quaziquote
has `
as its syntactic
sugar and comma
has ,
. We can
use quaziquote
to fix our letq
function as follows:
(set 'importq (macro (target value)
`(import (quote ,target))))
;; (quaziquote (import (quote (comma target))))
lust> (macroexpand (importq foo))
=> (import (quote foo))
Macros are probably the most powerful and interesting part of Lust. This manual saves the best for last. Macros are functions that are evaluated before the evaluator actually runs and who's arguments aren't evaluated when passed in. In other languages this is essentially what the preprocessor does, but in Lust and most Lisp languages because lists are simultaneously code and data, it is trivial to use a macro to generate complex code at compile time.
Macro's are probably best demonstrated with an example. Let's
take a look at the when
macro from
the Lust homepage. Here's what it looks
like:
(setq when (macro (cond body) `(if ,cond ,body ())))
Let's walk through how something like (when 1 2)
is
evaluated.
(if 1 2 ())
(if 1 2 ())
and returns 1
as 1
is a truthy value in Lust.
This seems simple, and it is, but it can take quite some time to
wrap your head around. If you're still confused I've
found this write up
to be extremely helpful. For debugging macros you'll also find
the macroexpand
function to be helpful.
macroexpand
macroexpand
is a function that expands and does not
evaluate a macro. It can be extremely useful when debugging
issues with macros. Here's an example of it being used in the
Lust REPL.
lust> (when 1 2)
=> 2
lust> (macroexpand (when 1 2))
=> (if 1 2 ())