s7 playground

A work-in-progress interactive tutorial of s7 scheme. For a complete manual please refer to https://ccrma.stanford.edu/software/snd/snd/s7.html

Table of Contents

Intro

unbound-variable
Hello there!!
;unbound variable this-will-cause-an-error in this-will-cause-an-error

Math

s7 includes:

  • sinh, cosh, tanh, asinh, acosh, atanh
  • logior, logxor, logand, lognot, logbit?, ash, integer-decode-float
  • random
  • nan?, infinite?
(1 0 33)


Other math-related differences between s7 and r5rs:

  • rational? and exact mean integer or ratio (i.e. not floating point), inexact means not exact.
  • floor, ceiling, truncate, and round return (exact) integer results.
  • # does not stand for an unknown digit.
  • the "@" complex number notation is not supported ("@" is an exponent marker in s7).
  • +i is not considered a number; include the real part.
  • modulo, remainder, and quotient take integer, ratio, or real arguments.
  • lcm and gcd can take integer or ratio arguments.
  • log takes an optional second argument, the base.
  • . and an exponent can occur in a number in any base.
  • rationalize returns a ratio!
  • case is significant in numbers, as elsewhere: #b0 is 0, but #B0 is an error.
(1 0 578)


define*, lambda*

define* and lambda* are extensions of define and lambda that make it easier to deal with optional, keyword, and rest arguments.

The syntax is very simple: every argument to define* has a default value and is automatically available as a keyword argument. The default value is either #f if unspecified, or given in a list whose first member is the argument name. The last argument can be preceded by :rest or a dot . to indicate that all other trailing arguments should be packaged as a list under that argument's name. A trailing or rest argument's default value is () and can't be specified in the declaration. The rest argument is not available as a keyword argument.

((1 32 "hi") (3 2 "hi") (3 2 1))


Macros

Macros is where the lisp languages really shine. They can extend the language in ways otherwise not possible. They are also quite tricky to get, personally it took me some time to understand the concept.

My short explanation that I wish I had read somewhere else:

  • When you call a normal function (my-function (+ 1 2)), the argument (+ 1 2) gets evaluated and its result gets passed to the function. So, in the function body, that argument already has the value 3
  • On the contrary, when you call a macro, the macro accepts exactly what you have typed. Meaning, upon calling (my-macro (+ 1 2)), the argument inside the macro is the exact list you typed, meaning (+ 1 2) and not 3 which is the result you'd get after evaluation. So, in other words, you have to evaluate the arguments inside the macro yourself to get their value.

Demonstrating this in code:

(+ 1 2)
my-function: x is 3
my-macro: x is (+ 1 2)

An example with if. Let's say if wasn't available in the language, and we will construct my-if. We want to pass 3 arguments to my-if

  • First, will be the test clause.
  • Then will be our 2 branches.

If the test clause is not false, we shall execute (aka evaluate) the 1st branch (aka 2nd argument), otherwise the 2nd branch (aka 3rd argument).

To summarize, the signature shall be (my-if test-clause branch-true branch-false).

  • Q: Could we implement it as a function?
  • A: No! As we said above, when we call a function, all its arguments are evaluated. Let me demonstrate:
2
executing branch-true
executing branch-false

You see? But in our my-if we don't want the branch-true to be evaluated if not necessary! That's why we need a macro.

2
executing branch-false

You see the difference? Before we go on explaining how you write a macro, let me show you a different Implementation of my-if

2
executing branch-false

These 2 implentations are essentially the same.

Fun with macros

Internally in this document I'm using the examples macro to showcase small snippets and their results. That saves me from writing the result myself. I only write the code snippets wrapped around the examples macro call, and evaluate it to get the <code snippet> ;; => <result> output

<unspecified>
(+ 1 2 1) ;; => 4
(/ 10 2) ;; => 5

Benchmarks

Let's see how fibonacci is doing

(9227465 0.192625)


The above (cached) result is with s7 running on my desktop machine. Click eval to see the performance on the browser (don't forget to eval the previous code block that defines time)

Below are some benchmark times in my machine.

fibonacci : wasm

Wasm file size / benchmark. (fib x) time in seconds. File size of .wasm included as well.

optimization file size (fib 38)
-O2 2mb 5.5
-Os 1mb 5.5
-O0 3.8mb 8.1

The equivalent fib implentation in javascript (running in the browser) took 0.45s for fib(38). Try opening a console and running benchmarkFib(38).

fibonacci : desktop

Desktop comparison (see src/repl.c)

optimization file size (fib 38) (fib 41)
-O0 3.2mb 1.4 6.1
-O2 10mb 0.8 3.4
-Os 5.3mb 0.8 3.4

Similar implementation in other languages, and time in seconds for fib 41 (including boot time)

lang fib 41
node v10.19 3
chez scheme 2.5