July 30, 2025 by Lex Plt5 minutes
This article is still a work in progress, send me your thoughts on Mastodon!
According to Paul Graham, there are 9 important aspects that differentiate Lisp from other languages. From those 9, ArkScript has 8 (it does not have garbage collection, but instead relies on scopes, like C++, its host language).
Let’s see how ArkScript differs from other Lisp.
ArkScript tries to be functional, by providing all the tools needed to the user to be used as a functional language. However it still supports imperative programming, and its standard library makes heavy use of it, where it can improve performances.
Common Lisp supports functional programming, while also allowing mutable state and imperative programming like ArkScript, while Clojure strongly encourages (sometimes insists) that you program in a functional way.
let
in Common Lisp creates a scope for local variables, and it has a body using the declared local variables. In ArkScript, let
is used to declare an immutable variable and doesn’t return anything.
;; Common Lisp
(let ((a 6)
(b 4))
(+ a b))
;; ArkScript
(let a 6)
(let b 4)
(let res (+ a b))
Common Lisp uses a defun
macro, which may include documentation:
(defun square (x)
"Calculates the square of the single-float x."
(declare (single-float x) (optimize (speed 3) (debug 0) (safety 1)))
(the single-float (* x x)))
In ArkScript, we use the keyword fun
and ArkDoc to document our functions:
# @brief Calculates the square of the float x
# @param x a floating number
# =begin
# (print (square 11.2)) # => 125.44
# =end
# @author https://github.com/SuperFola
(let square (fun (x)
(* x x)))
ArkScript has tail call optimizations but no first-class continuation support, Clojure has no TCO (due to compiling for the JVM) and no first-class continuation support either.
However, both languages support continuation-passing-style since they both support anonymous functions.
ArkScript supports:
true
and false
(list)
(equivalent to []
), [1 2 3]
; list
is a builtin, []
is syntactic sugar(dict)
, (dict "key" "value")
; dict
is a builtin and we must follow the function call syntax of ArkScript to use it-1
, 0.123465
, 123e2
"hello world"
nil
(fun (a b c) ())
(fun (&capture &x) ())
ArkScript does not have type annotations, nor static type checking (as of writing).
Types are strong but dynamic, meaning they can change at runtime, eg:
(mut i 5)
(set i "hello") # perfectly valid
Checks on types are only done when calling builtins or operators.
Like programs in many other programming languages, ArkScript uses names to refer to functions and variables, which are subject to scope. There are two ways to create a scope:
while
loop: the body has its own scopeEverything defined outside of those two will be created in the global scope.
ArkScript uses dynamic scopes: variable names are then resolved to values, by looking in the current scope, and then each upper scope until we hit the global one.
Example:
(let f (fun (callback) {
(let f_data 5)
(callback) }))
(let g (fun () {
(print (format "{} - {}" f_data global_value)) # 5 - 12
42 }))
(let global_value 12)
(f g)
Then there is namespacing, that can affect how variables are named. Let’s say we have two files, main.ark
and lib.ark
.
Importing lib
from main
will prefix all global variables in lib.ark
with lib:
and put them all in the global scope of main.ark
(importing main
from another file foo.ark
won’t create main:lib:var
but instead give you access to lib:var
).
Example:
# main.ark
(import lib)
(lib:foo 1 2)
We can also import only a few select variables, to refer to them by their name without prefix (we can still call them with their prefix to remove any ambiguity).
Let’s say we have three files, main.ark
, b.ark
and c.ark
all in the same folder:
# b.ark
(let var "b.ark")
# c.ark
(let var "c.ark")
# main.ark
(import b :var)
(import c :var)
(print (format "var={} b:var={} c:var={}" var b:var c:var))
# Will print:
# var=b.ark b:var=b.ark c:var=c.ark
Since b
is imported first, var
without prefix will resolve to b:var
.
Creating a closure in ArkScript is as easy as creating a function and explicitly capturing one or more variables:
(let create (fun (name code)
(fun (&name &code) (print (format "{}: {}" name code)))))
(let bob (create "Bob" 14))
(let charly (create "Charly" 27))
(print bob) # (.name=Bob .code=14)
(if (= 14 bob.code)
(print "ok"))
One can create a semblance of object system using closures, but closures are a poor man’s object.
ArkScript has non-hygienic macros, as it is handling macros like code transformations:
(macro foo (head 1))
,(macro using_a (e) {
(let a 42)
e })
(let four {(using_a (/ a 10))})
(print four) # 4.2
ArkScript has no reader macros, only value, function and conditional macros, meanwhile Common Lisp has user-definable reader macros.
All Lisp variants seen here have some kind of quoting, which ArkScript does not have. In many places, quoting may be used to have a shorter expression, eg:
;; Sorts the list using the > and < function as the relational operator.
(sort (list 5 2 6 3 1 4) #'>) ; Returns (6 5 4 3 2 1)
(sort (list 5 2 6 3 1 4) #'<) ; Returns (1 2 3 4 5 6)
In ArkScript we would have to write an anonymous function or use a predefined function, since functions are first-class:
(my_sort (list 5 2 6 3 1 4) (fun (a b) (< a b)))
# or
(let comp (fun (a b) (< a b)))
(my_sort (list 5 2 6 3 1 4) comp)