This is supposed to be an introduction into zepto. If you know Scheme already, I would advise you to read through the Syntactic Additions article, which details the differences from R5RS.
The syntax of Lisp derivatives is incredibly simple. It is infamous for having a lot of brackets, but that is for a reason. Expressions are not grouped by binding power, like in most other languages, but instead by brackets. Inside a bracket, there has to be a single function and its arguments, such as:
(+ 1 1 1)
Note that every operator is a function in Scheme (and thus, zepto), so it is always written in prefix notation as opposed to the infix notation we are used to. This has the benefit of being able to provide it as much arguments as we want to, e.g. for calculating the sum of multiple elements.
Let's see zepto in action on a more complex example:
(define (abs x) "returns the absolute of x" (if (>= x 0) x (* x -1)))
That is a lot of information all at once. Let us step through
define keyword binds the first argument to the second
argument, e.g. in
(define x 10). It is also used to define functions.
The name of the function we define is abs and it takes a single argument
x. We also gave define an optional docstring so that when we ever forget
what abs is, we can just do
(help abs) (for Clojure users there is also
a convenience alias called
doc), which will print the docstring together
with the function signature. After that comes the function body. Functions
are always a single statement, except when they are grouped by
which we will discuss later. Here we check whether the number is bigger
than 0 and if so, return the number (that is not an operation, so it does
not need brackets). If it is not, we are multiplying it with -1 in order
to make it positive. That does need brackets, because it is a function.
Bear with me so far? Good. Let us move on then.
Lisp has a famous concept regarding numbers that has found its way
into a lot of different languages, such as Smalltalk and most Lisp-like
languages. The basic notion
of numbers in zepto is this: there is the most fundamental number type,
which is the integer. You can safely promote it to all kinds of numbers,
because they all sit in higher spots in the tower, but you cannot always
safely demote them without losing precision (e.g. 1.3 is a float that has no
equivalent integer). You can call
round and such, but you will lose
precision that way.
So what types are supported?
(Small Int->)Integer->Float->Rational->Complex 10s 100 2.3 4/3 1+1i
To illustrate, I have made a little diagram of the tower. At least
integers should be pretty familar to you. They are of arbitrary
precision, which means that you can represent any integer, although
working with them will become very slow as they become extremely
big, and any float up until ~1.7e308 (any float bigger than that
is "Infinity"). That is not true arbitrary precision, but it should
When a float has hit the fan, it cannot be saved. This essentially
(- (+ 1.7e308 0.1e308) 0.1e308) will still yield "Infinity".
A new concept that zepto introduces is Small Ints, which basically wrap
hardware numbers (so, depending on your machine 32bit or 64bit ints).
That in turn allows for faster number crunching, but also for all the
disadvantages that the hardware brings, namely wrap-arounds. If your
numbers are getting too big and the hardware cannot display them anymore,
they will not behave as expected anymore. Up until now, they can only
be created explicitly by calling
(make-small 1), and I intend to keep
it that way. They can be dangerous, so I advise you to refrain from
using them extensively unless you have proven that it will help you
gain a significant speedup. In the REPL, they are displayed with a
s, like so:
zepto> (make-small 100) 100s
When working with any other number type (even Integers) they are promoted. Before explaining how the Rational and Complex number types work, let me give you a quick list of standard functions that work with numbers:
(+ 1 2) ; add two or more values (- 1 2 3) ; substract two or more values or negate a single value (* 1 2 3 4 5) ; multiply two or more values (/ 1 2) ; divide two or more values ; more on arithmetic operations are to be found in the Math wiki page (= 1 1.0) ; compare two values (number? 1) ; check whether arg is a number (integer? 1.3) ; check whether arg is an integer (rational? 1/3) ; check whether arg is rational (real? 1+40i) ; check whether arg is real (null? 4) ; check whether arg is null; generic for other data types (exact? 3) ; is number exact (inexact? 1/40) ; is number inexact (even? 20+20i) ; is number even (odd? -20) ; is number odd (zero? 0) ; is number zero; only for numbers (positive 10e2) ; is number positive (negative 10e2) ; is number negative (complex? 1) ; is number complex (exact->inexact 1/2) ; make exact number from inexact number (integer->float 1.5) ; make float from integer (gcd 1 400) ; greatest common divisor of two numbers (lcm 1+0i 400) ; least common multiple of two numbers (abs -19+-12i) ; absolute number of arg pi ; returns pi as a rational e ; returns e as a rational (make-small 40) ; make small from integer (char->integer #\Λ) ; smake integer from char, conforming to ASCII/Unicode; this is a captial lambda, by the way (string->number "400") ; make number from string, can be any number type except small
Those are not a lot, but certainly enough to do a few basic experiments and probably more.
After having discussed those things, let me explain Rationals.
They are actually a pretty smart way to overcome a problem as old
as computers - I am able to say that it is smart, because it is
easy, effective and not my idea. You have actually seen them before,
namely in above list, but probably not recognized as such.
In Scheme derivatives, a Rational is represented as two numbers
divided by a slash and no spaces, e.g.
1/30000000. The Rational
is not evaluated to a Float, instead two numbers are kept in memory.
This allows us to represent very numbers precisely instead of cutting
digits off at some point. Cool, isn't it?
The Complex type is a bit peculiar. If you do not know what a complex number is, you will likely not need this type and even if you do, you might not have use for it. If you think that is the case, just skip this section and jump to the next. You're not missing anything major, except if you ever try to calculate the square root of a negative number and expect the program to die.
The Complex type is represented as two numbers, also without space,
joined by a plus and with a trailing i, such that they look like that:
as well as Rationals exist in many languages, the really killer feature
in zepto in that regard is that those number types are first-class
That should conclude our fiddling with numbers, let's move on to chars and strings.
There is a syntactic addition to zepto that may confuse Scheme, Lisp and Clojure
programmers initially, albeit for different reasons. But I am getting
ahead of myself. First of all, zepto has a basic notion of symbols like any
other language in the Lisp realm. This allows for constructs such as
(define x "foo") and the like; in other words, values can be bound to names.
This is wonderful, but there is a piece of syntactic sugar that is elementary to zepto, but not necessarily to other Scheme interpreters, which is Atoms. Now, before any of you Clojure guys jump to conclusions, it is not what you call Atoms. In Clojure and Common Lisp land, this concept is known as Keywords. I simply decided to use them Atoms, because Keywords is a terrible name for that concept in my opinion. You are free to disagree with me on that.
For all of you not familiar with what I just ranted about, Atoms are simply symbols
with an inherent value. They stand for themselves, i.e.
:ok is just that, it is
:ok . You also cannot assign something to them, as in
(define :ok "ok").
So why not just use
"ok" then? Of course that would be possible, but unelegant and, in zepto, unidiomatic.
If you feel like arguing with me on that - and you really should! Don't just drink
the Kool-Aid. -, think of it like that: strings are text, not a notion of something
programmatic. Atoms are essentially facts, strings are not (necessarily).
How to use Atoms then? Just keep in mind
that strings are mostly used when we need a textual representation of something.
If we need a programmatic representation of some higher level concept, use Atoms.
Let me give you a slightly silly example for when to use Atoms:
(define (run f) "runs f and wraps booleans in atoms" (if (f) :ok :error))
We could now do something like
(run (lambda () #t)), which would yield
I hope you can see by now that using Atoms looks a bit cleaner than passing around strings all the time. There are a few functions that check for common Atoms:
(ok? :ok) (error? :error) (yes? :yes) (no? :no)
There is a little caveat on the whole "Atoms remain unevaluated" thing. As you may
have already noticed by now, REPL keywords (such as
quit) share their syntax with
Atoms. They are not the same thing, but they look the same. So typing keywords in the
REPL can differ from a scripts' behaviour (where you have to use
(quit) to end a script explicitly. There is an ongoing discussion (within myself) whether I
should replace one or the other with
#, but I have not settled yet.
For Clojure people: Other than in Clojure, using Atoms in zepto does not result in a significant speedup in zepto. They are just a different syntactic concept to keep your code clean, not a funky data structure. In fact, a bit of profiling shows they might even be (very slightly) slower than Strings at the moment. That will change eventually but do not expect them to be blazingly fast any time soon.
The first thing to note is that Chars and Strings are different data types in zepto, as is convention in Scheme. That means that Strings are not just an array of characters, but rather a basic building block in itself. There are ways to let those two types interact, of course, but let us begin by just introducing the types itself before working on making them play nicely.
Let's start with Chars.
Chars have a somewhat awkward syntax in Scheme and, for that matter,
zepto. If you want to use a literal character a, you will have to
#\a. This might look weird to you, but at least literal Unicode
characters are supported (an example can be found in
There are a few functions for working with characters:
(char? 1) ; check whether arg is char (char-lower-case #\A) ; convert char to lower case (char-upper-case #\a) ; convert char to upper case (char=? #\C #\d) ; checks whether chars are equal (char #\B #\Y) ; checks whether one char is less than another (char>? #\l #\ä) ; checks whether one char is greater than another (char<=? #\q #\m) ; you know the drill by now (char>=? #\Ü #\á) ; are we through yet? ; all of the above functions are also available in ; case insensitive versions: (char-ci=? #\L #\l) (char-ci #\@ #\\) (char-ci>? #\X #\#) (char-ci<=? #\€ #\$) ; if you type #\€ into your repl, you will likely not ; display what you would expect. But it works, promise. (char-ci>=? #\l #\m)
Chars are normally not all that interesting by themselves, though,
but rather when they're joined together, namely as strings. As I
already told you, Strings in zepto are a datatype that is seperate
from characters which means that they are more than just
'(#\l #\i #\s #\t #\s). This allows for greater flexibility.
Many standard functions behave a lot as if they were, though.
There are also a few functions for working with Strings in zepto,
(string? #\a) ; checks whether arg is string (string=? #\a) ; checks wether strings are equal (string #\a) ; you already know all those functions (string>? #\a) (string<=? #\a) (string>=? #\a) (string-ci=? #\a) (string-ci #\a) (string-ci>? #\a) (string-ci<=? #\a) (string-ci>=? #\a) (string '(#\w #\o #\w)) ; the string "constructor"; takes one or more characters (make-string 10) ; another string constructor. it takes the initial length as integer (string-length "doge") ; gets length of string (symbol->string duck) ; creates a string from symbol (list->string [#\s]) ; behaves as the constructor, but takes only lists (string-copy "lol") ; returns a copy of a string (substring "zepto" 1 3) ; returns a substring delimite by two integers (string-ref "lisp" 1) ; returns a reference to a string element (as char) (string-find "scheme" #\e) ; returns first occurrence of the element as index (string-append "a" "b") ; appends char or string to string
The purists among you may notice some deviations from R5RS. Most of it is by design, although if you think some of this is unacceptable behaviour, create a pull request of file an issue, I am always open to suggestions.
Programmatically, strings are not used as often. Atoms are used more frequently.
This concludes our short introudction to Strings in zepto. Next up are Lists and Vectors.
I lied. This section is not only about Lists and Vectors, but also about Quasiquoted Lists and Dotted Lists, two data structures which behave similarish to Lists, but are not the same.
Let's talk about Lists. Lists are heterogenous collections. They can be used
like arrays in other languages, but they are actually much more than that.
You can also see them as unevaluated code, really, e.g.
'(+ 1 2 3) is a valid
List. The little
' is the symbol that means that the following brackets are
not code, but data. You can read this as
quote, which makes sense - at least
to me. If you come from a programming language like Python or Ruby,
you might find the alternative square bracket notation - you can write the above
[+ 1 2 3] - rather convenient. The quoted expression or list can later
be transformed into code by calling eval on it, i.e.
(eval '(+ 1 2 3)) will return
Lists are not your run-of-the-mill arrays then. You can use them as such, but that can be somewhat clumsy. If you want to have high-performance, plain-old-data collections, I'd suggest you go for Vectors. Before explaining the concept of Vectors, I will again supply you with a list of functions for working with Lists:
(list? [foo bar baz]) ; is arg a list (list 1 2 3) ; create list from elements (head [1 2 3]) ; get head (first element) of list (tail [1 2 3]) ; get tail of list (indexed-tail [1 2 3] 2) ; get tail of list beginning at index (list-tail [1 2 3] 2) ; alias for tail (cons 1) ; construct list (car [+ 1 2]) ; alias for head (cdr [+ 1 2]) ; alias for tail (cadr [+ 1 2]) ; get head of tail (cdar ...) (cddr ....) [...] (cddddr ...) ; this is the longest version of it in the stdlib (list-ref [+ 1 2 3] 2) ; get element from list at index (vector->list #(1 2 3)) ; make list from vector (string->list "hi") ; makes list from string (list-append '(a) "b") ; appends element or list to list
Let's move on to Vectors. Vectors are optimized for random access. They are fast, they are small. But they are also less cool than list, because they are only data and cannot become code, except through converting it to a list before. As such, you can use them wherever you want to efficiently manipulate data, but nothing too meta. As an aside: there is an experimental matrix library in the standard library that wraps vectors of vectors and makes working with such data easier; if you ever find yourself in a position where you have to do matrix manipulation, feel free to have a look at it. There is a section on matrices in the Math chapter.
There is another syntax addition to zepto that is a bit more interesting than Atoms, namely List Comprehensions. They are a way of building lists from different lists by walking through every element in the existing list and applying some kind of mutation to it, possibly also filtering those elements.
List comprehensions look like this:
; [mutation | element-name <- list, optional-filter] [(+ x 1) | x <- [1 2 3 4], (> x 2)] ; this will return [4 5] [(display el) | el <- '(:h "i")] ; this will print :hi and return an empty list
They are a handy construct to express a map-and-filter operation concisely. I hope they are as straightforward to you as I deem them to be.
Zepto (c) 2015 Veit Heller