Language Reference

CircuitScheme is a language from the Lisp family based on Lispy by Peter Norvig as described in (How to Write a (Lisp) Interpreter (in Python)) and (An ((Even Better) Lisp) Interpreter (in Python)). As such, it is very similar to Scheme and much of the text here is taken more or less verbatim from the MIT Scheme reference manual. 

Data Types

Booleans represent true and false. Boolean literals are #t and #f for true and false, respectively. The only thing that is considered to be logically false is #f. Everything else is logically true, including 0 and the empty list, which may surprise some.

Numbers are exactly as they are in Python.

Strings are any sequence of characters other than " enclosed by a pair of ", e.g. "string". If you need to have " in a string, use \".

Symbols are simple identifiers, e.g. function-name. Symbols follow the follow 4 simple rules:

  1. can only contain graphic characters (i.e. no control characters)
  2. can not contain any of the characters:();,"`&[]{}\
  3. can not begin with a number or single quote
  4. and not contain whitespace

Typically, - is used to separate words in a symbol, _ is used in special symbols (such as system use) to separate words and as a prefix and suffix. The characters ?, !, and * are typically used as the final character of a function name to denote:

? a predicate, e.g. null?

! a mutating function (changes the argument rather than returning a modified copy), e.g. set!

* a variant of the primary function, e.g. flatten (which does a one level flattening of a list) and flatten* (which is a recursive flatten)

Lists are the central data type in any Lisp. They are simply a non-homogeneous sequence of data items (as above) and/or other lists, surrounded by parentheses: (1 a #t (1 2 3) "hello").

Functions are user defined procedures. They are covered in detail later.

Macros are user defined syntactic extensions.

Additionally, any piece of CircuitPython data can be used in CircuitScheme, although they can only be created in Python code. See the Hardware Extension section for an example.

Special Forms

(lambda formals sexpr...)

A lambda expression evaluates to a procedure. The environment in effect when the lambda expression is evaluated is remembered as part of the procedure; it is called the closing environment. When the procedure is later called with some arguments, the closing environment is extended by binding the variables in the formal parameter list to the arguments according in order, left to right. The new environment created by this process is referred to as the invocation environment.

Once the invocation environment has been constructed, the sexprs in the body of the lambda expression are evaluated sequentially in that environment. This means that the region of the variables bound by the lambda expression is all of the sexprs in the body. The result of evaluating the last sexpr in the body is returned as the result of the procedure call.

formals, the formal parameter list, is often referred to as a lambda list.

Examples:

(lambda (x) (+ x x)) ⇒

((lambda (x) (+ x x)) 4) ⇒ 8

(define reverse-subtract
  (lambda (x y)
    (- y x)))
(reverse-subtract 7 10) ⇒ 3

(define foo
  (let ((x 4))
    (lambda (y) (+ x y))))
(foo 6) ⇒ 10

(let ((variable init)...) sexpr...)

The inits are evaluated in the current environment (in some unspecified order), the variables are then bound (in an environment extending the one the let is being evaluated in) to fresh locations holding the respective results, the sexprs are evaluated sequentially in the extended environment, and the value of the last sexpr is returned. Each binding of a variable has the sequence of sexpr as its region.

Note that the following are equivalent:

(let ((variable init)...) expression...)
((lambda (variable...) expression...) init...)

Some examples:

(let ((x 2)
      (y 3))

  (* x y)) ⇒ 6

(let ((x 2) (y 3))
  (let ((foo (lambda (z) (+ x y z)))
        (x 7))
    (foo 4))) ⇒ 9

Definitions

(define variable sexpr)
(define formals sexpr...)

Definitions may only occur at the top level of a program and at the beginning of a lambda body: that is,
the body of a lambda,let, or procedure define expression. A definition that occurs at the top level of a program is called a top-level definition, and a definition that occurs at the beginning of a body is called an internal definition.

The second form is used as a shorthand to define a procedure. We saw earlier how the result of lambda can be used as a variable value. The first item in formals is not a parameter but the name of the resulting procedure; thus formals cannot be empty.

Hence the following are identical.

(define inc (lambda (x) (+ x 1)))
(define (inc x) (+ x 1))

Using this form of define, a function that accepts a completely option set of arguments can be made:

A top-level definition,

(define variable sexpr)

has essentially the same effect as this assignment expression, if variable is bound. I.e. it binds a new value to the symbol.

(set! variable expression)

If variable is not bound, however, define binds variable to a new location in the current environment before performing the assignment (it is an error to perform aset! on an unbound variable).

(define add3(lambda (x) (+ x 3))) ⇒ unspecified
(add3 3) ⇒ 6

(define first car) ⇒ unspecified
(first '(1 2)) ⇒ 1

An internal definition is a definition that occurs at the beginning of a body (that is, the body of a  lambda, let, or procedure define expression), rather than at the top level of a program. The
variable defined by an internal definition is local to the body. That is, variable is bound rather than assigned, and the region of the binding is the entire body. For example,

(let ((x 5))
  (define foo (lambda (y) (bar x y)))
  (define bar (lambda (a b) (+ (* a b) a)))
  (foo (+ x 3))) ⇒ 45

Assignment

(set! variable expression)

expression is evaluated and the resulting value is stored in the location to which variable is bound. The value of the set! expression is unspecified.

variable must be bound either in some region enclosing the set! expression, or at the top level.

(define x 2) ⇒ unspecified
(+ x 1) ⇒ 3
(set! x 4) ⇒ unspecified
(+ x 1) ⇒ 5

Quoting

This section describes the expressions that are used to modify or prevent the evaluation of objects.

(quote datum)

This evaluates to datum which may be any external representation of a CircuitScheme object. Use quote to include literal constants in code.

(quote a) ⇒ a
(quote (+ 1 2)) ⇒ (+ 1 2)

(quote datum) may be abbreviated as 'datum. The two notations are equivalent in all respects.

'a ⇒ a
'(+ 1 2) ⇒ (+ 1 2)
'(quote a) ⇒ (quote a)
''a ⇒ (quote a)

Numeric constants, string constants, character constants, and boolean constants evaluate to themselves, so they don't need to be quoted.

'"abc" ⇒ "abc"
"abc" ⇒ "abc"
'145932 ⇒ 145932
145932 ⇒ 145932
'#t ⇒ #t
#t ⇒ #t

(quasiquote template)

Backquote or quasiquote expressions are useful for constructing a list structure when most, but not all of the desired structure is known in advance. If no commas appear within the template, the result of evaluating is equivalent to the result of evaluating 'template. If a comma appears within the template, however, the expression following the comma is evaluated (i.e. unquoted) and its result is inserted into the structure instead of the comma and the expression. If a comma appears followed immediately by an at-sign (@), then the following expression must evaluate to a list; the opening and closing parentheses of the list are then stripped away and the elements of the list are inserted in place of the comma at-sign expression sequence. quasiquote, comma, and comma-at are equivalent to `, ,, ,@, respectively.

`(list ,(+ 1 2) 4) ⇒ (list 3 4)

(let ((name 'a)) `(list ,name ',name)) ⇒ (list a 'a)

`(a ,(+ 1 2) ,@(map abs '(4 -5 6)) b) ⇒ (a 3 4 5 6 b)

`((foo ,(- 10 3)) ,@(cdr '(c)) . ,(car '(cons)))
⇒ ((foo 7) . cons)

`,(+ 2 3) ⇒ 5

Quasiquote forms may be nested. Substitutions are made only for unquoted components appearing
at the same nesting level as the outermost backquote. The nesting level increases by one inside each successive quasiquotation, and decreases by one inside each unquotation.

`(a `(b ,(+ 1 2) ,(foo ,(+ 1 3) d) e) f)
⇒ (a `(b ,(+ 1 2) ,(foo 4 d) e) f)

(let ((name1 'x)
      (name2 'y))
  `(a `(b ,,name1 ,',name2 d) e)) ⇒ (a `(b ,x ,'y d) e)

The above notations and (quasiquote template) are identical in all respects and is identical to .

(quasiquote (list (unquote (+ 1 2)) 4))
⇒ (list 3 4)

'(quasiquote (list (unquote (+ 1 2)) 4))
⇒ `(list ,(+ 1 2) 4)

Unpredictable behavior can result if any of the symbols quasiquote, unquote, or unquote-splicing appear in a template in ways otherwise than as described above.

Macros

(define-macro (formals) template)

Create a named macro:

formals is the same as in a procedure definition: a name followed by formal parameters,
if any. NOTE that the arguments to a macro invocation are not evaluated, but
are passed as is to the macro to do with as it wishes.

template the template expression that is processed when the macro is invoked. The result of evaluating the processed template expression becomes the value of the macro invocation. template is typically (even always) a quasiquoted expression using the formal parameter names for purposes of unquotiing in order to fill in the template.

(define-macro (double x)
  `(+ ,x ,x))

(double 5) ⇒ 10

Sequencing

The begin special form is used to evaluate expressions in a particular order.

(begin expression...)

The expressions are evaluated sequentially from left to right, and the value of the last expression is returned. This expression type is used to sequence side effects such as input and output. Keep in mind, begin does not create a nested lexical environment.

(define x 0)
(begin (set! x 5)
       (+ x 1)) ⇒ 6

(begin (display "4 plus 1 equals ")
       (display (+ 4 1)))
⇒ unspecified

It prints "4 plus 1 equals 5".

Often the explicit use of begin is unnecessary, because many special forms already support sequences of expressions (that is, they have an implicitbegin):

  • cond
  • define ;''procedure define'' only
  • lambda
  • let

Conditionals

The behavior of the conditional expressions is determined by whether objects are true or false. The conditional expressions count only #f as false. Everything else, including #t, lists, symbols, numbers, strings, and procedures count as true.

In the descriptions that follow, we say that an object has a true value or is true when the conditional expressions treat it as true, and we say that an object has a false value or is false when the conditional expressions treat it as false.

(cond clause...)

Each clause has this form: (predicate expression...) where predicate is any expression. The last clause may be an else clause, which has the form: (#t expression...)

A cond expression does the following:

  1. Evaluates the predicate expressions of successive clauses in order, until one of them evaluates to a true value.
  2. When a predicate evaluates to a true value, cond evaluates the expressions in the clause in left to right order, and uses the result of the last evaluation as the result of the entire cond expression.
  3. If all predicates evaluate to false values, and there is no else clause, the result of the conditional expression is unspecified; if there is an else clause, cond evaluates its expressions (left to right) and returns the value of the last one.

    (cond ((> 3 2) 'greater)
          ((< 3 2) 'less)) ⇒ greater
    (cond ((> 3 3) 'greater)
          ((< 3 3) 'less)
          (#t 'equal)) ⇒ equal

Normally, programs should not depend on the value of a cond expression that has no else clause. However, some prefer to write cond expressions in which at least one of the predicates is always true. In this style, the final clause is equivalent to an else clause.

(and expression...)

The expressions are evaluated from left to right, and the value of the first expression that evaluates to a false value is returned. Any remaining expressions are not evaluated. If all the expressions evaluate to true values, the value of the last expression is returned. If there are no expressions then #t is returned.

(and (= 2 2) (> 2 1)) ⇒ #t
(and (= 2 2) (< 2 1)) ⇒ #f
(and 1 2 'c '(f g)) ⇒ (f g)
(and) ⇒ #t

(or expression...)

The expressions are evaluated from left to right, and the value of the first expression that evaluates to a true value is returned. Any remaining expressions are not evaluated. If all expressions evaluate to false values, #f is returned. If there are no expressions then #f is returned.

(or (= 2 2) (> 2 1)) ⇒ #t
(or (= 2 2) (< 2 1)) ⇒ #t
(or #f #f #f) ⇒ #f
(or) ⇒ #f

(if predicate consequent [alternative])

predicate, consequent, and alternative are expressions. An if expression is evaluated as follows: first, predicate is evaluated. If it yields a true value, then consequent is evaluated and the result is returned. Otherwise alternative is evaluated and the result is returned. If predicate yields a false value and no alternative is specified, then the result of the expression is unspecified.

An if expression evaluates either consequent or alternative, never both. Programs should not depend on the value of an if expression that has no alternative.

(if (> 3 2) 'yes 'no) ⇒ yes
(if (> 2 3) 'yes 'no) ⇒ no
(if (> 3 2)
    (- 3 2)
    (+ 3 2)) ⇒ 1

(when predicate expression...)

when is a macro based on if.

If predicate evaluates to a true value, the sequence of expressions is evaluated and the result of the last one is the result of the when form, otherwise the result is undefined.

(when (> x 5)
  (write-line "greater")
  (+ x 2))

The above is equivalent to the following, but is simpler and clearer.

(if (> x 5)
  (begin (write-line "greater")
         (+ x 2)))

(unless predicate expression...)

unless is also a macro based on if.

If predicate evaluates to logically false, the sequence of expressions is evaluated and the result of the last one is the result of the unless form, otherwise the result is undefined.

(unless (> x 5)
  (write-line "greater")
  (+ x 2))

The above is equivalent to the following, but is much simpler and clearer.

(if (> x 5)
  ()
  (begin (write-line "greater")
         (+ x 2)))

Evaluation

(apply function list)

Apply the function that results from evaluating function to the argument list resulting
from evaluating list which, as indicated, must result in a list.

(apply + '(1 2)) ⇒ 3

(eval expression)

Evaluate expression in the current environment.

(eval '(+ 1 2)) ⇒ 3

Type tests

(boolean? object)

Returns whether object is a boolean.

(list? object)

Returns whether object is a list.

(null? object)

Returns whether object is an empty list.

(pair? object)

Returns whether object is a non-empty list. While CircuitScheme does not actually build its lists from pairs (see a scheme/lisp introduction for more on this), instead using Python lists directly, the name for the concept is still used.

(symbol? object)

Returns whether object is a symbol.

(port? object)

Returns whether object is a port, i.e. a file-like object.

Numerical Operations

(+ number1 number2)
(* number1 number2)

(- number1 number2)
(/ number1 number2)

These work as expected.

(not object)

Results in the logical negation of object. Keep in mind that anything other than #f is a true value.

(not #t) ⇒ #f
(not #f) ⇒ #t
(not 0) ⇒ #f

Operations and constants from CircuitPython's math module are directly available from CircuitScheme. E.g.

(sin 3.4) ⇒ -0.255541
pi ⇒ 3.14159

Numeric Comparisons

These work as expected.

(= number1 number2)
(< number1 number2)
(> number1 number2)
(<= number1 number2)

(>= number1 number2)

Equality

(equal? object1 object2)

This is equivalent to CircuitPython's eq operator.

(eq? object1 object2)

This is equivalent to CircuitPython's is operator.

List Operations

(list object...)

Returns a list of its arguments.

(list 'a (+ 3 4) 'c) ⇒ (a 7 c)
(list) ⇒ ()

These expressions are equivalent:

(list OBJ1 OBJ2 ... OBJN)
(cons OBJ1 (cons OBJ2 ... (cons OBJN '()) ...))

(cons obj1 list)

Returns a newly allocated pair whose car is obj1 and whose cdr is list (which must be a list).

(cons 'a '()) ⇒ (a)
(cons '(a) '(b c d)) ⇒ ((a) b c d)
(cons "a" '(b c)) ⇒ ("a" b c)

(append list1 list2)

Returns a list consisting of the elements of list1 followed by the elements of
list2.

(append '(x) '(y)) ⇒ (x y)
(append '(a) '(b c d)) ⇒ (a b c d)
(append '(a (b)) '((c))) ⇒ (a (b) (c))

(car list)

Returns the first item of list (i.e. list[0] in Python). Note that taking the car of the empty list results in an error.

(car '(a b c)) ⇒ a
(car '((a) b c d)) ⇒ (a)

(cdr list)

Returns the contents of the of list, other than the first item (i.e. list[1:] in Python). Note that taking the cdr of the empty list results in the empty list.

(cdr '(a b c d)) ⇒ (b c d)
(cdr '()) ⇒ ()

(length list)

Returns the length of list.

(length '(a b c)) ⇒ 3
(length '(a (b) (c d e))) ⇒ 3
(length '()) ⇒ 0

Input/Output

(open-input-file filename)

Takes a filename referring to an existing file and returns an input port capable of delivering characters from the file. Specifying a non-existent file results in an error. This is equivalent to open(filename) in CircuitPython.

(open-output-file filename)

Takes a filename referring to an output file to be created and returns an output port capable of writing characters to a new file by that name. This is equivalent to open(filename, 'w') in CircuitPython.

(close-input-port port)
(close-output-port port)

Closes port and returns an unspecified value. The associated file is also closed.

(write object [output-port])

Writes a representation of object to output-port (which defaults to the standard output stream (stdout), and returns an unspecified value. If object has a standard external representation, then the written representation generated by write shall be parsable by read into an equivalent object. Thus strings that appear in the written representation are enclosed in double-quotes, and within those strings backslash and double-quote are escaped by backslashes. write performs discretionary output flushing and returns the number of characters written.

(display object  [output-port])

Works as write does except that strings are written without surrounding quotes.

(newline [output-port])

Writes an end-of-line to output-port (defaults to stdout), and returns an unspecified value.

(read [input-port])

Converts external representations of CircuitScheme objects into the objects themselves. read returns the next object parsable from input-port (defaults to the standard input stream (stdin)), updating input-port to point to the first character past the end of the written representation of the object. If an end of file is encountered in the input before any characters are found that can begin an object, read returns an end-of-file object. The input-port remains open, and further attempts to read will also return an end-of-file object. If an end of file is encountered after the beginning of an object’s written representation, but the written representation is incomplete and therefore not parsable, an error is signaled.

(read-char [input-port])

Read a single character from input-port (defaulting to stdin).

(eof-object? object)

Returns #tif object is an end-of-file object; otherwise returns#f.

(load filename)

Reads and evaluates code from the file named by filename. If filename doesn't end in .scm it is appended.

Call-with-current-continuation (aka call/cc)

call/cc provides us with a capability comparable to Python's try/except mechanism. This section is cribbed from Norvig's description since it's so good.

Here are some, examples:

(call/cc (lambda (throw)
           
(+ 5 (* 10 (call/cc (lambda (escape)
                            
        (* 100 (escape 3))))))))
⇒ 35

(call/cc (lambda (throw)
           
(+ 5 (* 10 (call/cc (lambda (escape)
                                 (* 100 (throw 3))))))))
⇒ 3

In the first example, evaluating (escape 3)aborts the current calculation and returns 3 as the value of the enclosing call to call/cc. The result is the same as (+ 5 (* 10 3)) or 35.

In the second example, (throw 3) aborts up two levels, throwing the value of 3 back to the top level.

In general, call/cc takes a single argument, proc, which must be a procedure of one argument. proc is called, passing it a manufactured procedure which here is called throw. If throw is called with a single argument, then that argument is the value of the whole call to call/cc. If throw is not called, the value computed by proc is returned. 

This guide was first published on Feb 14, 2019. It was last updated on Feb 14, 2019. This page (Language Reference) was last updated on Jul 16, 2019.