Skip to content

Latest commit

 

History

History
403 lines (341 loc) · 11.5 KB

index.org

File metadata and controls

403 lines (341 loc) · 11.5 KB

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

Intro

;; these are some comments
(display "Hello there!!") ;; stdout is displayed in blue
(+ 1 2 3)
;; comment the following line (prepend ";;")  and you'll see 6 as a result
this-will-cause-an-error
;; stderr is displayed in red
;; note: the result you get is the result of the last evaluated expression

Math

s7 includes:

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

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.
(exact? 1.0) ;; => #f
(rational? 1.5) ;; => #f
(floor 1.4) ;; => 1
(remainder 2.4 1) ;; => 0.3999999999999999
(modulo 1.4 1.0) ;; => 0.3999999999999999
(lcm 3/4 1/6) ;; => 3/2
(log 8 2) ;; => 3
(number->string 0.5 2) ;; => 0.1
(string->number "0.1" 2) ;; => 0.5
(rationalize 1.5) ;; => 3/2
(complex 1/2 0) ;; => 1/2
(logbit? 6 1) ;; => #t
(list
 (cos 0)
 (sin 0)
 (random 1000))

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.

(define*
 ;; function name: hi
 ;; a is needed
 ;; b has default argument 32
 ;; c has default argument "hi"
 (hi a (b 32) (c "hi"))
 ;; just returning the arguments
 (list a b c))

(list
 (hi 1)
 (hi :b 2 :a 3)
 (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:

(define (my-function x)
  (format #t "my-function: x is ~A\n" x)
  x)

(define-macro (my-macro x)
  (format #t "my-macro: x is ~A\n" x)
  ;; macros need to return some list ` is just a handy construct
  `',x ;; the `',x causes to return the argument as passed: a list
  )

(my-function (+ 1 2))
(my-macro (+ 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:
(define (my-if-function test-clause branch-true branch-false)
  ;; yeah yeah, we use here the normal if but..
  ;; cannot do this in another way!
  (if test-clause branch-true branch-false))

(my-if-function
 #f ;; let's run the 2nd one
 (begin
   (display "executing branch-true\n")
   1)
 (begin
   (display "executing branch-false\n")
   2))

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.

(define-macro (my-if-macro test-clause branch-true branch-false)
  `(if ,test-clause
       ,branch-true
       ,branch-false))

(my-if-macro
 #f ;; let's run the 2nd one
 (begin
   (display "executing branch-true\n")
   1)
 (begin
   (display "executing branch-false\n")
   2))

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

(define-macro (my-if-macro2 test-clause branch-true branch-false)
  (if (eval test-clause)
      (eval branch-true)
      (eval branch-false)))

(my-if-macro2
 #f ;; let's run the 2nd one
 (begin
   (display "executing branch-true\n")
   1)
 (begin
   (display "executing branch-false\n")
   2))

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

(define-macro (examples . args)
  `(for-each (lambda (exp)
	       (format #t "~A ;; => ~A\n" exp (eval exp)))
	     ',args))

(examples
 (+ 1 2 1)
 (/ 10 2)
 )

Benchmarks

(define-macro (time expr)
  `(let ((start (*s7* 'cpu-time)))
     (let ((res (list ,expr))) ; expr might return multiple values
       (list (car res)
	     (- (*s7* 'cpu-time) start)))))

Let’s see how fibonacci is doing

(define (fib n)
  (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))

(time (fib 35))

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.

optimizationfile size(fib 38)
-O22mb5.5
-Os1mb5.5
-O03.8mb8.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)

optimizationfile size(fib 38)(fib 41)
-O03.2mb1.46.1
-O210mb0.83.4
-Os5.3mb0.83.4

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

langfib 41
node v10.193
chez scheme2.5