slashbinbash.de / Sikkel / Language

Sikkel (extension: .sik) is a Lisp language. It borrows ideas from Common Lisp, Scheme, Racket, and Clojure.

Go to the download page

If you don't know what Lisp is, you should read the Introduction to Lisp article. This should give you enough information to understand the contents of this page.

General

Comments are prefaced by a semicolon ';' and can appear anywhere in the code:

; Comment

Data-Types

The built-in data-types are:

NameExample
Booleantrue or false
Integer42
String"Hello World!"
Symbol+, even?, bob
List(24 true ("Hello World!" bob) cat (42))

Arithmetic

(+ 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

Comparisons

(= 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

Logic

Boolean operations can only be used with boolean values.

(not ture)
    => 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 and or 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.

Conversions

(string->symbol expr)  
(symbol->string expr)  
(integer->string expr)

Variables

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).

Evaluation

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

Function

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'

Lambda

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

Closure

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.

Do

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.

Conditions

If

(if (< 3 8) 10 16)

(if (= 3 8)
    (print "A")
    (print "B"))

The if form requires you to specify a true and false expression.

Cond

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.

Case

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 euqality. You can test the equality of either one or more values. The character '_' denotes the else case and is required.

Match

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

Loops

Repeat

(repeat 5 (print "Test"))

While

(define i 5)

(while (> i 0)
    (print "i:" i)
    (set i (- i 1)))

Until

(define i 5)

(until (= i 0)
    (print "i:" i)
    (set i (- i 1)))

Scope

There are three namespaces where variable and function definitions live:

  1. built-in namespace
  2. global namespace
  3. function namespace

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

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

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)))

Modules

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.

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'

Sikkel Module Files

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)

Misc

Assert

(assert (< 3 4) "good")
(assert (= 3 4) "bad")

Defun'

When using the defun function definition, all parameters are evaluated before they are passed to the function.

With defun' the parameters are not evaluated. This is equivalent to using defun and using quote on each parameter.

(require list)

(defun foo (a b c) (list c b a))

(defun' bar (a b c) (list c b a))

(foo (+ 1 2) (- 3 2) (* 3 3))
    => (9 1 3)
(foo (quote (+ 1 2)) (quote (- 3 2)) (quote (* 3 3)))
    => ((* 3 3) (- 3 2) (+ 1 2))
(bar (+ 1 2) (- 3 2) (* 3 3))
    => ((* 3 3) (- 3 2) (+ 1 2))

Further Reading