Basic Control Flow
Let’s jump right in and take a look at some GDLisp code. Once you’ve
built GDLisp, you should have an executable called
gdlisp. If you invoke this executable with no arguments, you’ll be
placed in a read-eval-print loop, or REPL for short. In this
interactive interpreter, you can type GDLisp expressions or
declarations and see the results live. This interpreter is backed by a
running Godot instance, so you also have access to the full breadth of
built-in Godot types and functions, like Node and sqrt.
Hello, world!
Let’s start with everybody’s favorite program.
(print "Hello, world!")
If you type this into the REPL and hit enter, you’ll see “Hello,
world!” printed to the screen. This calls the built-in function
print with one string argument, namely the string “Hello, world!”.
Congratulations! You’ve just written your first line of GDLisp.
If you just ran this, you’ll notice that, in addition to the string,
the interpreter also printed out a stray set of parentheses ().
This is normal and is for good reasons. In GDLisp, every expression
returns a value, even those like print that are usually only
called for their side effects. Since print has no meaningful
return value, it returns the nil value as a default, which is written
as (). So when you see (), you can read that as “nil”.
Note
You might wonder why the null value prints out as (),
rather than as the word “nil”. This is because () is
used as a placeholder for the empty list and is used as the
final cdr cell in a proper list. You can read more about how
lists are actually stored in GDLisp at The GDLisp Parser.
Let’s do some math.
(+ 1 1)
This will, naturally, add one and one together and return the result:
two. Operators like + and - are written in prefix notation in
GDLisp, in contrast to GDScript and most non-Lisp languages, which use
infix notation. In fact, + isn’t an operator at all; it’s just a
function like print. GDLisp is very liberal in what it considers a
valid function name, and + is a perfectly valid identifier.
The + function also supports variable arguments. The following
will add all four numbers together and print the total:
(+ 1 2 3 4)
Since everything is written as prefix function calls, there’s never any ambiguity about operator precedence.
(* 3 (+ 1 1) (+ 1 10 (* 2 4)))
All of the common mathematical operators work as functions in exactly
the way you’d expect. + is for addition, * is for
multiplication, - is for subtraction, and / is for division.
All of the operators support variable arguments, with associativity to
the left when it matters (so, for instance (/ a b c) is equivalent
to (/ (/ a b) c), not (/ a (/ b c))).
Data Types
GDLisp supports the same basic data types as GDScript, albeit with different syntax.
The GDLisp equivalent of Godot’s “null” value is called “nil” and is
written in GDLisp as (). GDLisp also defines a global constant
called nil, which evaluates to ().
The Boolean values “true” and “false” are written, respectively, as
#t and #f.
Integers and floats are written in GDLisp in the same way as in GDScript.
Strings are written in GDLisp using double quotes "...". Strings
cannot be written using single quotes, as the single quote character
is used for something different in GDLisp. Aside from that, all of the
usual escape sequences work. Additionally, GDLisp supports two methods
of escaping Unicode values in strings. The first consists of \u
followed by exactly four hexadecimal digits, exactly like GDScript’s
syntax for the same. The second consists of \u{...}, with any
number of hexadecimal digits enclosed in the curly braces. This syntax
can be used to represent any Unicode code point, including those
outside the basic multilingual plane.
Conditionals
If Expressions
The most basic conditional in GDLisp is the if macro.
(if some-conditional true-case false-case)
if is a macro that takes three arguments. The first argument is
the conditional, which is evaluated according to Godot’s rules of
truthiness. That is, zero, false, the empty string, and other
intuitively “empty” objects are considered false.
(if (> x 0)
(print "x is positive!")
(print "x is not positive!"))
This expression tests whether the variable x is positive and, in
either case, prints out an appropriate message. Note that >, like
+, is a perfectly valid identifier in GDLisp and is in fact a
simple, built-in function just like +.
Up to this point, we’ve been calling these forms “expressions”
somewhat informally, but it’s important to note that even control
forms like if are expressions in GDLisp, whereas in a language
like GDScript or Python if would be considered a statement. Put
more colloquially, if is really no different than any other
expression and can also return values. So we could’ve written our
print like so.
(print (if (> x 0) "x is positive!" "x is not positive!"))
Or we could have returned the resulting string from a function, or assigned it to a variable, or any number of other things. GDLisp does not distinguish between statements and expressions. Anything that can be used in expression context returns a value.
The true and false branches of the if statement are each
individual arguments, which means each branch expects a single
expression. If you need to perform multiple actions inside one of the
branches, you can use the progn Forms special form.
(if (> x 0)
(progn (print "x is positive!")
(print "Have a lovely day :)"))
(print "x is not positive!"))
progn is a special form that takes zero or more arguments,
evaluates each of them in order, and then returns the value of the
last one.
Finally, the “false” branch is optional and will default to the nil
expression ().
(if (> x 0)
(progn (print "x is positive!")
(print "Have a lovely day :)")))
Though if your intention is to execute some code conditionally for its side effects, you might find the macros when and unless more useful.
Cond Expressions
Extending our if expression from above, we might consider adding
another case to distinguish between negative numbers and zero.
(if (> x 0)
(print "x is positive!")
(if (< x 0)
(print "x is negative!")
(print "x is zero!")))
Nesting additional if branches in an “else” block is a very common
pattern. Languages like Python and GDScript accommodate this with an
elif keyword. GDLisp accommodates this pattern with the cond
expression, the general-purpose conditional dispatch form. cond
takes the following form.
(cond (conditional1 branch1) (conditional2 branch2) ...)
That is, the word cond is followed by several lists. Each list
consists of a conditional expression, similar to the conditional
portion of if, followed by one or more expressions. When
evaluated, the cond form evaluates each conditional. When it
encounters one that’s true, it stops, evaluates that branch, and
returns the result. Our if above can be translated as follows:
(cond
((> x 0) (print "x is positive!"))
((< x 0) (print "x is negative!"))
(#t (print "x is zero!")))
If none of the branches match, then cond silently returns (),
though in many cases (including our example above), it’s common to
include a final “catch-all” clause whose conditional is the literal
true #t value.
Like if, cond is an expression and returns values, so the
entire cond expression can be assigned to a variable or passed as
a function argument.
Note
Incidentally, cond is the only primitive conditional
expression in GDLisp. if, when, unless, and,
and or are all macros built on top of cond, while
cond is baked into the compiler.
Comparisons
We’ve already seen the < and > functions, which compare values
for, respectively, the less-than and greater-than relations. The
<= and >= functions also exist for non-strict comparisons.
Finally, = is used for equality and /= is used for inequality.
Comparison, ordering, and equality semantics are equivalent to the
corresponding Godot operators.
All of these functions accept variable arguments and are applied
transitively to their arguments. Concretely, that means that (< a b
c d) is true if and only if a is less than b, b is less
than c, and c is less than d. The other comparison
operators work similarly.
Of particular note is /=, which is unique among the comparison
operators in that it is not transitive. (/= a b c d) is true if
and only if none of the arguments are equal. It compares every
possible pairing of arguments, not just the adjacent ones. More
concretely, (/= 1 2 1 3) is false, since one is equal to itself.
Since the = function obeys Godot’s built-in equality semantics, it
compares dictionaries by reference. The GDLisp function equal?
works like = but for deep equality. equal? is similar to the
GDScript function deep_equal, except that the former accepts
variable arguments and applies the equality relationship transitively.
Looping Expressions
Like GDScript, GDLisp provides constructs for repeating code.
While Loops
The simplest looping construct is a while loop. To print all of
the numbers from 10 down to 0, we can write:
(let ((x 10))
(while (> x 0)
(print x)
(set x (- x 1))))
The while form takes a conditional expression as its first
arguments, and the loop body, which can consist of zero or more
expressions, follows. When a while loop is evaluated, the
conditional is evaluated. If the conditional is true, then the body is
evaluated and the process repeats. If the conditional is false, then
the body is skipped. while loops always return ().
GDLisp has no direct equivalent of a “do-while” loop from other
languages. However, the construct is easy enough to mimic. Since the
conditional part of a while loop can be any expression (even a
progn form), we can simply place the entire loop body inside the
conditional part and leave the body empty.
(let ((x 10))
(while (progn (print x)
(set x (- x 1))
(> x 0))))
For Loops
For loops in GDLisp work exactly as they do in GDScript.
(for i (range 10)
(print x))
for takes a variable name, an iterable object (such as an array or
a string), and a body. It evaluates the body once for each element of
the iterable, with the variable bound to that element at each
iteration. Like while, a for loop always returns ().
Breaking and Continuing
The built-in special forms (break) and (continue) work like
the equivalent GDScript keywords. (break) can be used inside of a
loop and, when evaluated, will immediately exit the loop.
(continue) can be used inside a loop and jumps back to the
beginning of the loop, beginning the next iteration of the loop (or
exiting the loop, if we were on the final iteration).
How NOT to Loop
Now that we’ve seen the two basic looping constructs in GDLisp, it’s
important to emphasize that there are often alternatives. In an
imperative language like GDScript or Java, most iteration is done, as
a matter of course, with for or while. However, GDLisp is a
functional programming language, and as such it provides several
higher-order functions designed to capture common looping and
iteration patterns. This section aims to provide an incomplete summary
of some of the useful functions that can be used to replace common
looping patterns.
Transforming each Element of an Array
Consider this GDScript code.
var new_array = []
for element in old_array:
new_array.push_back(element * 2)
return new_array
This block of code takes an array old_array, doubles each element,
and returns a new array containing the doubles. This pattern of
applying an operation to each element of an array is captured by the
array/map function. The idiomatic way to write the
above code in GDLisp is
(array/map (lambda (x) (* x 2)) old-array)
Summing or Combining all Elements of an Array
This GDScript code sums the elements of an array.
var total = 0
for element in old_array:
total += element
return total
The pattern of accumulating all of the elements of an array using some binary function is called a fold, and it can be done in GDLisp with array/fold.
(array/fold #'+ old-array 0)
Discarding some Elements of an Array
The following GDScript code takes an array and keeps only the elements which are positive.
var new_array = []
for element in old_array:
if element > 0:
new_array.push_back(element)
return new_array
This pattern is captured by the array/filter function.
(array/filter (lambda (x) (> x 0)) old-array)
Searching an Array for some Matching Element
This GDScript code searches an array, in order, for an element satisfying a particular condition, returning the first match.
for element in old_array:
if element % 10 == 0:
return element
return null
This can be done in GDLisp with array/find.
(array/find (lambda (x) (= (mod x 10) 0)) old-array)
array/find also optionally accepts a third argument, which
defaults to () and is returned if no match is found.
Locals
Local Variables
As we start to write larger blocks of code, it will become convenient
to store the results of intermediate expressions in local variables.
We’ve already seen examples that use the most basic primitive:
let.
(let ((variable-name1 initial-value1)
(variable-name2 initial-value2) ...)
...)
The let form takes a list of variable clauses and then a body. The
given variables are declared and bound to their initial values. Then
the body is evaluated in a scope where the local variables exist.
You can declare any number of variables in a let clause.
Crucially, the variables’ scope is bound strictly to the let
block, so as soon as the body of the let finishes evaluating, the
variables are no longer accessible. This is in contrast to GDScript,
where a var declaration implicitly lasts until the end of the
enclosing scope. In GDLisp, a local variable always defines its own
scope when it’s declared.
Example:
(let ((a 1) (b 2))
(+ a b)) ; 3
Note that when a let block has multiple variable clauses, the
clauses are evaluated in parallel.
(let ((x 1) (y (+ x 1))) y)
In this example, the x from the first clause is not in scope
when the y clause is evaluated. If there’s an x from an
enclosing scope available, then y will be set to that variable
plus one. If not, this code will produce an error at compile-time.
It’s common to want several variable bindings to be evaluated in order, such as when initializing several pieces of data to perform some calculations. For this use case, GDLisp provides the let* macro.
(let* ((x 1) (y (+ x 1))) y) ; 2
let* works exactly like let and shares all of the same syntax,
except that the variables in a let* are bound sequentially, with
each subsequent variable having access to all of the prior ones. In
effect, a let* form is equivalent to several nested let forms,
each having only one variable clause.
To change the value of an existing local variable, use set.
(let ((x 1))
(print x) ; 1
(set x (+ x 1))
(print x)) ; 2
Note that local variables, even mutable local variables, fully support
closures. That is, if a lambda or other local function construct
closes around a local variable, then both the closure function and the
enclosing scope can modify the variable, and both scopes will reflect
the change.
Local Functions
Similar to let, GDLisp provides the flet primitive for
declaring one or more local functions. This is most useful when you
have a private function that is only needed inside one function.
(flet ((normalize-name (name) ((name:capitalize):substr 0 10)))
(print "Hello, " (normalize-name first-name) (normalize-name last-name))
(print "You have an unread message from " (normalize-name caller-name)))
In this hypothetical example, we need to print some messages involving
people’s names. For consistency in our hypothetical UI, we want all
names to be capitalized in a particular way and to truncate their
length to ten characters. We can define the function to normalize
these names as a local function with flet and then call it as many
times as we want inside the flet block. Outside the block, the
function doesn’t exist.
labels is a variant of flet with the same syntax but more
powerful binding semantics. labels evaluates its function bodies
in a scope where all of the function clauses already exist. This
allows your local helper functions to be recursive or to depend on
each other in any order.
Tip
If you’re declaring a local function with the intent of
passing it (as an argument) to a higher-order function like
array/map or array/filter, you’ll have better luck
using an ordinary let and a lambda, since you need to
reify the function as a value anyway. flet and labels
are intended for situations where you want to locally call
the function inside the scope.
Comments
GDLisp line comments start with a semicolon.
; This is a line commentBy convention, a single semicolon is used to annotate a line of code, and a pair of semicolons is used when a line comment stands on its own line.
Block comments begin with
#|and are terminated by|#. Block comments cannot be nested.