Sikkel (Extension: .sik
) is a Lisp language.
Download Sikkel Interpreter (sikkel-20230906.zip)
To run the interpreter, you will need a Java 17 Runtime Environment.
java -jar sikkel.jar script.sik
Comments are prefaced by a semicolon ';'
and can appear anywhere in the code:
; Comment
The built-in data-types are:
Name | Example |
---|---|
Boolean | true or false |
Integer | 42 |
String | "Hello World!" |
Symbol | + , even? , bob |
List | (24 true ("Hello World!" bob) cat (42)) |
(+ 2 3)
=> 5
(+ 10 20 30 40 50)
=> 150
(- 8)
=> -8
(- 10 3)
=> 7
(* 3 9)
=> 27
(* 1 2 3 4)
=> 24
(/ 10 2)
=> 5
(mod 6 2)
=> 0
(= 3 3)
=> true
(< 9 3)
=> false
(> 9 3)
=> true
(<= 8 8)
=> true
(>= 9 4)
=> true
(boolean? 5)
=> false
(integer? "test")
=> false
(list? 18)
=> false
(string? 18)
=> false
(symbol? "test")
=> false
You can also test if values are in a certain range:
(define x 50)
(<= 0 x 100)
=> true
Boolean operations can only be used with boolean values.
(not true)
=> false
(not false)
=> true
(and true true)
=> true
(and true true false true)
=> false
(or false false)
=> false
(or false false true false)
=> true
(xor true false)
=> true
(xor true true)
=> false
and
, or
, and xor
are implemented as short-circuit operators.
(and (integer? true) foobar)
=> false
(or (integer? 18) foobar)
=> true
The second expression is never evaluated because the first expression evaluates to a boolean value that determines the result for the entire expression.
(string->symbol expr)
(symbol->string expr)
(integer->string expr)
(string->chars expr)
(chars->string expr)
(string->list expr)
(list->string expr)
Variables are defined using the define
function:
(define x 24)
(define y (* 10 2))
(define z (+ x y))
(define x 0)
=> error: 'x' already defined
Trying to redefine a variable in the same namespace will result in an error. If the variable is already defined, you can change its value by using set
.
(set! x 24)
Variables are defined in either the global scope of the module, the function scope, or scoped forms (see scope
, let
, match
).
List evaluation can be manipulated with quote
and eval
.
(quote ())
=> ()
(' (1 2 3 4 5 6))
=> (1 2 3 4 5 6)
(' (+ 3 4))
=> (+ 3 4)
(eval (' (+ 3 4)))
=> 7
Functions are defined in the Common Lisp style:
(defun add1 (x) (+ x 1))
(add1 18)
=> 19
(defun test (x y)
(print "Test")
(* x y))
The function definition can have multiple expressions. The return value of a function is the result of the last expression.
You can define variables inside the function body. The variables are only visible in the body.
(defun test (x y)
(define z 10)
(* x y z))
(test 2 3)
=> 60
z
=> error: undefined symbol 'z'
You can create an unnamed function by using lambda
:
(lambda (x) (+ x 1))
((lambda (x y) (* x y)) 10 5)
=> 50
Lambda functions are the most restrictive function type. Any variables that you want to use, you have to pass as a parameter.
You can give the lambda function a name:
(define add1 (lambda (x) (+ x 1)))
(add1 3)
=> 4
A closure is a lambda but with state. When you use closure
, all variables that are defined in its surrounding scope are captured:
(defun create-closure (a)
(closure (x) (* a x))) ; captures a
(define closure1 (create-closure 5))
(closure1 10)
=> 50
Note that captured objects cannot be garbage collected until the reference to the function object is removed.
Partial function application using partial
:
(defun sum (x y) (+ x y))
(define sum3 (partial sum 3))
(sum3 7)
=> 10
Function composition using compose
:
(defun inc (n) (+ n 1))
(defun dec (n) (- n 1))
(defun dup (n) (* n 2))
(dec (dup (inc 10)))
=> 21
((compose inc dup dec) 10)
=> 21
Applying functions in left-to-right order using pipe
:
(pipe 10 inc dup dec)
=> 21
Calling function with argument list using apply
:
(apply + (list 1 2 3 4))
=> 10
Some forms only allow one expression. You can use do
to bypass this restriction.
(do
(print "First")
(print "Second")
(+ 3 18))
The return value of do
is the result of the last expression.
(if (< 3 8) 10 16)
(if (= 3 8)
(print "A")
(print "B"))
The if form requires you to specify a true and false expression.
The cond
form is a list of condition and expression pairs.
(cond
((< 9 3) (print "A"))
((= 5 18) (+ 3 3))
(true (* 10 2)))
A condition that evaluates to true
is required.
The case
form is very similar to switch-statements in other languages. The values in each case are compared to the result of the value expression. If any of them matches, the case expression is evaluated.
(case (+ 1 2)
(1 (print "A"))
((2 3 4) (print "B"))
(_ (print "C")))
The separate cases are compared by equality. You can test the equality of either one or more values. The character '_'
denotes the else case and is required.
The match
form is a pattern matching function that compares values for equality and also assign variables if needed.
(require list)
(defun foo (x y)
(if (> x 5)
(list (' ok) x y)
(list (' err) "error")))
(match (foo 8 2)
((ok 8 5) (print "eight five"))
((ok 8 _) (print "eight any"))
((ok %x %y) (print (+ x y)))
((err %m) (print m))
(_ (print "any")))
If we call (foo 8 5)
, the function returns (ok 8 5)
. This matches with the first statement (ok 8 5)
perfectly, and "eight five"
is printed.
If we call (foo 8 2)
, the function returns (ok 8 2)
. This does not match the first statement. The second statement contains the wildcard character '_'
, which matches anything. "eight any"
is printed.
If we call (foo 10 20)
, the function returns (ok 10 20)
. This does not match neither the first nor the second statement. The third one however matches. The character '%'
tells the interpreter to do an assignment of any value that is found, to the given variable name. This means that 10 is assigned to x and 20 is assigned to y. The number 30 is printed.
If we call (foo 2 10)
, the function returns (err "error")
. Similar to the previous match, the error message is assigned to the variable m, and then printed.
The last case will match any other case that did not previously match.
Notes
There are three namespaces where variable and function definitions live:
The built-in namespace has definitions for all the standard language functions that can be used in any module. This includes functions for arithmetic, logic, conditions, loops, and others.
The global namespace contains the definitions of a module. These are the variables and functions that you define, the names from the modules that you import, etc.
The function namespace is a temporary namespace that is created when a function is called. It contains the values that you pass to the function, the variables that you define inside the function, etc.
In addition, there are functions that create temporary namespaces when variables are defined that would otherwise pollute the current namespace. For instance, match
can create a temporary namespace if you decide to assign values to variables.
scope
creates a temporary scope. The variables that are defined in the scope are invalidated once the the program leaves the scope. This allows you to define variables without polluting your namespace.
(define x 1)
(scope
(print x)
(define x 2)
(print x))
(print x)
Output:
1
2
1
The return value of scope is the result of the last expression.
let
is similar to scope
. You can create a temporary scope, define variables, and use them in an expression, all in one concise form.
(let ((a 5)
(b 10)
(c (+ a b)))
(* (+ b a) (+ a c)))
This is equivalent to writing:
(scope
(define a 5)
(define b 10)
(define c (+ a b))
(* (+ b a) (+ a c)))
The language comes with different modules. To import the names of a module, use the require
function.
(require list)
If you use it on the top level, it adds all definitions of list
to the global namespace of the current module.
You can restrict which names are imported into the namespace by using require-from
:
(require-from list (list append))
(append (list 1 2 3) (list 4 5 6))
=> (1 2 3 4 5 6)
(length (list 1 2 3 4))
=> error: undefined symbol 'length'
This is particularly useful if you only want to import a few names of a module that would otherwise import dozens of names.
To avoid name clashes between modules, you can import the names of a module with a given prefix using require-as
:
(require-as list lst/)
(require-as system sys-)
(lst/add (list 1 2 3 4) 5)
(sys-read-file "foo.txt")
The prefix is arbitrary but it must be a valid symbol.
You can use require
inside of a scope
or a function to import the names without polluting the global namespace:
(scope
(require list)
(list 1 2 3 4))
=> (1 2 3 4)
(defun test ()
(require list)
(list 1 2 3 4))
(test)
=> (1 2 3 4)
(length (test))
=> error: undefined symbol 'length'
(list 1 2 3 4)
=> error: undefined symbol 'list'
There are five built-in modules:
core
- main functionality of the language, imported by defaultfunctional
- function application formslist
- list manipulationstring
- string manipulationsystem
- I/O functions, functions of the underlying systemThe functions of each system are outlined in the reference document that is included in the downloaded archive.
Each Sikkel file is in itself a module and can be imported like a module. All names of a module are private by default. You have to specify which names should be exported when the module is loaded with provide
:
Foo.sik
(provide A B)
(define A 42)
(define B 10)
(define C 7)
To import the module defined by the Foo.sik file, use the require
function:
Bar.sik
(require Foo)
A
=> 42
B
=> 10
C
=> error: undefined symbol 'C'
If the module is located in a sub-directory, the path becomes part of the module name. For instance, the module address for the file "std/foo/bar.sik" is std.foo.bar
:
(require std.foo.bar)
There is limited support for Java integration:
public static
Java methodsboolean
int
String
The interpreter has to know where to find the classes before it can load them. You can add include directories and JAR files by using the -I
option:
java -jar sikkel.jar script.sik -I bin
java -jar sikkel.jar script.sik -I library.jar
Create a Java class
package de.slashbinbash.sikkel;
public class TestClass {
public static int testA(int a, int b) {
return a + b;
}
public static String testB() {
return "test";
}
}
Import all methods with class namespace import
(require system)
(import de.slashbinbash.sikkel.TestClass)
(TestClass.testA 10 15)
=> 25
(TestClass.testB)
=> "test"
Import all methods with import-static
(require system)
(import-static de.slashbinbash.sikkel.TestClass.*)
(testA 42 8)
=> 50
(testB)
=> "test"
Import single method with import-static
(require system)
(import-static de.slashbinbash.sikkel.TestClass.testA)
(testA 8 12)
=> 20
(testB)
=> error: undefined