Scheme and Racket are both dialects of the Lisp programming language, but they have significant differences in their design philosophies, features, and use cases.
Scheme is designed to be a minimalistic language, with a small core language and few built-in features.
Scheme is not a subset of Racket, but rather, Racket started as an implementation of the Scheme programming language and has since evolved to become a separate language with its own unique features.
The relationship between Scheme and Racket can be thought of more as a parent-child relationship. Racket was initially developed as a platform for Scheme systems, but it has since grown into its own language with features that extend beyond those found in Scheme: Racket includes many built-in features and libraries, including a powerful macro system, support for object-oriented programming, and extensive libraries for tasks such as web programming and GUI development.
Racket It also includes a built-in IDE, DrRacket which we will use to test our Racket code, though most of the time it will be also valid scheme code.
In Scheme, a simple function to add two numbers might look like this:
(define (add x y) (+ x y))
In Racket, the same function would be written like this:
#lang racket
(define (add x y) (+ x y))
The #lang racket
directive at the beginning of the file (.rkt
) tells DrRacket to choose the Scheme dialect.
Key aspects of Scheme to keep in mind:
Basic types:
#t
, #f
132897132989731289713
, 1.23e+33
, 23/169
, 12+4i
#\a
, #\Z
a-symbol
, another-symbol?
, indeed!
#(1 2 3 4)
"this is a string"
(1 2 #\a)
, (a . b)
, ()
empty listEvaluation of an expression produces a value, the evaluation of an expression (
The other sub-expressions (i.e.
The evaluation order of
lambda
is an unnamed procedure, definition and usage:
((lambda (arguments) function) parameters)
((lambda (x y) (+ (* x x) (* y y))) 2 3) ; =>13
Procedures are values, hence are first class objects.
let
is used for binding variables:
(let ((x 2)(y 3)))
Scoping rules are static, the following examples displays 1
:
(let ((a 1))
(let ((f (lambda ()
(display a))))
(let ((a 2))
(f)) ))
let
binds variables "in parallel", there's no need to use a tmp
variable to swap two variables:
(let ((x 1)
(y 2))
(let ((x y) ; swap x and y
(y x))
x)) ; =>2
let*
is a "sequential" variant of let, with it, x is bound before y.
if
, when
, unless
(if (predicate) consequent (alternative))
(when (condition) (then-part))
(unless (condition) (else-part)) ;which is equal to:
(when (not (condition)) (then-part))
We can use the begin
construct for writing a block of procedural code:
(begin
(op_1 ...)
(op_2 ...)
...
(op_n ...))
Every op_i is evaluated in order, and the value of the begin block is the value
obtained by the last expression.
named let
is used to create general loops in a "non-idiomatic" way:
(let ((x 0))
(let label () ; why an empty list?
(when (< x 10)
(display x)
(newline)
(set! x (+ 1 x))
(label)))) ; go-to label
quote
\ '
used to prevent evaluation (quote <expr>)
or '<expr>
.
quasiquote
`
and unquote ,
are used for partial evaluation.
'(1 2 3) ; = (quote (1 2 3)) => (1 2 3)
`(1 ,(+ 1 1) 3) ; = (quasiquote
; (1 (unquote (+ 1 1)) 3))
; => (1 2 3)
Variables created by let
are local. To create top-level bindings there is
define
, syntax: (define <name> <what>)
.
(define x 12) ; integer variable
(define y #(1 2 3)) ; mutable vector
(define z '(1 2 3)) ; mutable list
(define cube (lambda (x) (* x x x)))
(cube 3) ; => 27 procedure cube define with lambda, alternatively:
(define (square x) (* x x)) ; procedure defined with name and argument
(square 5) ; => 25
define
can be also used instead of let
in procedures.
set!
is for assignment:
(begin
(define x 23)
(set! x 42)
x) ; => 42
In general, procedures with side effects have a trailing bang !
character.
A predicate is a procedure that returns a Boolean, its name usually ends with ?
.
=
is used only for numbers.
eq?
tests if two objects are the same (good for symbols).
eqv?
like eq?
, but checks also numbers.
equal?
predicate is #t
if and only if the (possibly infinite) unfoldings of its arguments into regular trees are equal as ordered trees.
(null? list)
check if list is null, if not, return false
(eq? 'casa 'casa) ; #t
(eq? "casa" (string-append "ca" "sa")) ; #false
(eq? 2 2) ; is unspecified
(equal? (make-vector 5 ’a)(make-vector 5 ’a)) ; is true
The case
expression provides a way to test a single expression against multiple conditions and return a corresponding value.
It acts like a switch in c language, the predicate used in case
is eqv?
.
(case (car '(c d))
((a e i o u) 'vowel) ; vowel cases grouped together
((w y) 'semivowel)
(else 'consonant)) ; => 'consonant
The cond
function works by evaluating a series of conditions from top to bottom. When it finds the first condition that evaluates to #t
(true), it executes the corresponding expression and returns its value, then stops further processing. If no conditions are met, it evaluates the else
clause if present.
(cond ((> 3 3) 'greater)
((< 3 3) 'less)
(else 'equal)) ; => 'equal
(define a 1)
(define b 2)
(define c 3)
(a b c) ; => error
'(a b c) ; => (a b c)
In the first case, Scheme tries to apply a
(which is the number 1
) as a function to the arguments b
and c
, which results in an error because 1
is not a function. In the second case, Scheme treats (a b c)
as a list of symbols, so it returns the list as is courses.cs.washington.edu, gnu.org.
Lists are memorized as concatenated pairs.
A pair, which is written (x . y) and it is also called a cons node, consists of two parts: (car . cdr). The list '(1 2 3)
is stored like this (1 . (2 . (3 . ()))), where car=1 and cdr=(2 . (3 . ())).
'()
is the empty list also called nil in Lisp.
()
reads equal to (list)
(1 2 3)
reads equal to (list 1 2 3)
{1 2 3}
reads equal to (list 1 2 3)
[1 2 3]
reads equal to (list 1 2 3)
(1 (2) 3)
reads equal to (list 1 (list 2) 3)
(1 . 3)
reads equal to (cons 1 3)
(1 . (3))
reads equal to (list 1 3)
(1 '(3))
reads equal to (list 1 3)
(1 . 2 . 3)
reads equal to (list 2 1 3)
car
and cdr
can be merged together:
(car '(1 2 3)) ; => 1
(list (car '(1 2 3))) ; => '(1)
(cdr '(1 2 3)) ; => '(2 3)
(cadr '(1 2 3)) ; c-ad-r = car of a cdr => 2
cons
builds a pair (cons 1 2)
is '(1 . 2)
, (cons 1 '(2))
is '(1 2)
, which is a list: lists are stored as nested cons nodes where the head car is an element and the body with head cdr is a list.
append
to append a list to another one (append '(5 6) '(3 2 4)) ; => '(5 6 3 2 4)
take
to get the prefix of a list (take '(4 2 5 7 3) 3) ; '(4 2 5)
member
to check if a list contains a value (member 2 '(1 2 3)) ; => '(2 3)
.
apply
can be used to apply a procedure to a list of elements.
(apply + '(1 2 3 4)) ; => 10`.
for-each
, similar to apply, it applies a procedure recursively using each element of the list as argument.
(for-each (lambda (x) (display (* 2 x))) '(1 2 3 4)) ; => 2468
It is possible to define procedures with a variable number of arguments, passing them as a cons node:
(define (x . y) y)
(x 1 3 5 7) ; => '(1 3 5 7)
In this case we defined a function named x, which is also a cons node, and all the parameters are stored in y. In the general case we pass a list L as parameter:
(define (minimum L)
(if (null? (cdr L)) (car L)
(min (car L) (minimum(cdr L)))))
(define (minimum L) ; without binary op min
(let ((x (car L)) (xs (cdr L)))
(unless (null? xs) (minimum
(cons (if (< x (car xs)) x (car xs))
(cdr xs)) ))))
min
and max
are binary scheme procedures.
cons
vs list
Vectors are heterogeneous structures whose elements are indexed by integers. They are similar to arrays in other languages. Accessing an arbitrary element in a list requires a linear traversal, while arbitrary vector elements can be accessed in constant time.
#(1 apple 3)
reads equal to (vector 1 'apple 3)
#3("apple" "banana")
reads equal to (vector "apple" "banana" "banana")
#3()
reads equal to (vector 0 0 0)
vector-length
return the size of the vector.
vector-ref
gets the referenced value by index (vector-ref #(58 34 12 43) 3) ; => 43
.
vector-set!
set the element indexed by (vector-set! vect i val) ; vect[i] = val
.
In Scheme, literal constants are immutable, meaning their values cannot be changed once they are defined. They are treated as self-evaluating expressions, which means they evaluate to themselves. In Scheme, literal constants can include numbers, characters, strings, and boolean values. Here are some examples of literal constants in Scheme:
1
, 3.14
, -5
a
, b
, c
"Hello"
, "World"
#t
(true), #f
(false)Literal constants seen in the last examples are immutable, this means that procedures as vector-set!
can be applied only to a vector (or a list) initialized using let
(local) or define
(global).
Every Scheme implementation is required to be properly tail recursive. A procedure is called tail recursive if its recursive call is "at the tail", i.e., is the last operation performed, examples of both:
(define (factorial n) ; not tail recursive
(if (= n 0)
1
(* n (factorial (- n 1)))))
(define (fact n) ; tail recursive
(define (fact-tail x accum) ; define local procedure
(if (= x 0) accum
(fact-tail (- x 1) (* x accum ))))
(fact-tail n 1)) ; call local procedure
The not tail recursive implementation will expand first all the factors of the factorial (and return addresses, etc.) on the stack, this leads to use of unnecessary use of memory.
The tail recursive version makes use of a local procedure fact-tail defined with two arguments a number x
and an accumulator accum
.
If x
is zero, it directly returns the accumulated value, otherwise, it makes a recursive call to fact-tail
, decrementing x
by 1
and multiplying the accumulator by x
.
fact
calls fact-tail
once with parameter n
. The procedure fact-tail
won't do nested calls but will return immediately, calling itself again, using the partial result stored in the accumulator.
The recursive call is the last operation in the function, making it a tail-recursive function, meanwhile in the non tail-recursive version the last operation is *
.
Tail recursive procedures can be optimized to avoid stack consumption, indeed, the previous tail call is translated in the following low-level code:
(define (fact-low-level n)
(define x n)
(define accum 1)
(let loop () ; see this as the "loop" label
(if (= x 0)
accum
(begin
(set! accum (* x accum ))
(set! x (- x 1))
(loop)))))) ; jump to "loop"
Call by object sharing (like in Java): objects are allocated on the heap and references to them are passed by value. It is also often called call by value, because objects are evaluated before the call, and such values are copied into the activation record.
(define (test-setting-local d)
(set! d "Local") ; setting the local d
(display d)(newline))
(define ob "Global")
(test-setting-local ob) ; => Local
(display ob) ; => Global
The copied value is not the object itself, which remains in the heap, but a reference to the object; this means that, if the object is mutable, the procedure may exhibit side effects on it:
(define (set-my-mutable d)
(vector-set! d 1 "done")
(display d))
(define ob1 (vector 1 2 3)) ; i.e. #(1 2 3)
(set-my-mutable ob1) ; => #(1 done 3)
(display ob1) ; => #(1 done 3)
It is possible to define new types, through struct
, like in C but with some differences:
(struct being (
name ; name is immutable
(age #:mutable) ; flag for mutability
))
A number of related procedures are automatically created, e.g., the constructor being
and a predicate to check if an object is of this type: being?
in this case.
Also accessors (and setters for mutable fields) are created:
(define (being-show x)
(display (being-name x))
(display " (")
(display (being-age x))
(display ")"))
(define (say-hello x)
(if (being? x) ; check if it is a being
(begin
(being-show x)
(display ": my regards.")
(newline))
(error "not a being" x)))
(define james (being "James" 58))
(say-hello james) ; => James (58): my regards.
(set-being-age! james 60) ; a setter
(say-hello james) ; => James (60): my regards.
Structs can inherit, but not override:
(struct may-being
being ; being is the father
((alive? #:mutable)) ; to be or not to be
)
(define (kill! x)
(if (may-being? x)
(set-may-being-alive?! x #f)
(error "not a may-being" x)))
(define (try-to-say-hello x)
(if (and
(may-being? x)
(not (may-being-alive? x)))
(begin
(display "I hear only silence.")
(newline ))
(say-hello x)))
(define john (may-being "John" 77 #t))
(kill! john)
(say-hello john) ; John (77): my regards.
(try-to-say-hello john) ; I hear only silence.
The main difference is in methods vs procedures: procedures are external, so with inheritance we cannot redefine/override them: still a may-being
behaves like a being
. We have to define a new procedure (i.e. try-to-say-hello
), to cover the role of say-hello
for a may-being
.
begin
is a special form (a kind of function) that can take any number of expressions and execute them in sequence, if we remove it the outer parentheses will try to apply the result of (display "Hello, I'm ")
as a function, which is not possible because display
returns a void value that cannot be used as a function.
A closure is a function together with a referencing environment for the non-local variables of that function, i.e. a function object that "closes" over its visible variables.
(define (make-adder n)
(lambda (x)
(+ x n)))
(define x 5)
(define add5 (make-adder x))
(define sub5 (make-adder -5))
(= (add5 x) (sub5 15)) ; => #t
x ; => 5
make-adder
is defined with a fixed environment variable n that remains inside the function itself thanks to static scoping, then each time the function is called it applies the procedure to the local x
variable passed as argument.
A closure can be used as an iterator:
(define (iter-vector vec)
(let ((cur 0)
(top (vector-length vec)))
(lambda ()
(if (= cur top)
'<<end>>
(let ((value (vector-ref vec cur)))
(set! cur (+ cur 1)) value)))))
(define i (iter-vector #(1 2)))
(i) ; => 1
(i) ; => 2
(i) ; => ’<<end >>
map
takes a procedure and one or more lists , the procedure must have an arity equal to the number of list passed, if more list are passed they should have the same length:
(map (lambda (x) (+ 1 x)) '(0 1 2)) ; => '(1 2 3)
(map (lambda (x) (+ 1 x)) '(0 1 2) '(9 4 10)) ; => error = map: argument mismatch
(map (lambda (x y) (* y x)) '(0 1 2) '(9 4 10)) ; => '(0 4 20)
map
vs for-each
filter
takes a predicate and a list and return a filtered list:
(filter (lambda (x) (> x 0)) '(10 -11 0)) ; => '(10)
(filter positive? '(1 -2 3 4 -5)) ; => '(1 3 4)
foldr
and foldl
take a procedure, an initial value, and one or more lists. They apply the procedure between n elements at a time, starting respectively from left and right, using the last iteration result as the first element of next iteration:
(foldl cons '() '(1 2 3 4)) ; '(4 3 2 1)
(foldl (lambda (a b result)
(* result (- a b)))
1
'(2 2 2)
'(4 5 6)) ; (((1 * -4) * -3)) * - 2) => -24
(foldl string-append "" '("bella" "è" "vita" "la")) ; => "lavitaèbella"
(foldl (lambda (v t) (cons (+ 1 v) t)) '() '(1 2 3 4)) ; => '(5 4 3 2)
(foldr (lambda (v t) (cons (+ 1 v) t)) '() '(1 2 3 4)) ; => '(2 3 4 5)
Macros in Racket are a powerful tool that allows you to extend the language's syntax. They operate on syntax objects, which are essentially s-expressions or identifiers bundled with some information about their location in the source code.
A pattern-based macro replaces any code that matches a pattern and generates output syntax based on that pattern, this last step is called expansion. Macros are expanded at compile-time.
Macros can be defined through define-syntax
and syntax-rules
, the last is a pair (pattern expansion).
(define-syntax id
(syntax-rules ()
[(id . pattern) template])) ; general form
(define-syntax while
(syntax-rules () ; no other keywords needed
[(_ condition body ...) ; pattern P
(let loop () ; expansion of P
(when condition
(begin
body ...
(loop))))]))
(let ((x 0))
(while (< x 10)
(display x)
(set! x (+ x 1)))) ; => 0123456789
_
in the pattern stands for the macro's name while
, and body ...
is the code to be executed inside the loop. The ellipsis ...
means that there can be any number of body expressions, in particular ...
is used to represent zero or more repetitions of the preceding form.
The define-syntax-rule
form is itself a macro that expands into define-syntax
with a syntax-rules
form that contains only one pattern and one template. The previous case has only one pattern, it can be re-written as:
(define-syntax-rule (while condition body ...)
(let loop ()
(when condition
(begin
body ...
(loop)))))
In some cases syntax can be expanded in more than one way based on the pattern, lets re-write let*
as a recursive macro:
(define-syntax my-let*
(syntax-rules ()
[(_ ((var val)) istr ...) ; (= only one variable)
(( lambda (var) istr ...) val)]
[(_ ((var val) . rest) istr ...) ; more than one
(( lambda (var)
(my-let* rest istr ...)) val )]))
(let ((a 1)(b a)(c b))
(display e)) ; error => a: unbound identifier
(my-let* ((a 1)(b a)(c b))
(display c)) ; => 1
(let ((x 1)) ...)
can be expressed with a lambda form ((lambda (x) ...) 1)
.
syntax-rules
; Define a macro that matches patterns with literal identifiers `add` and `sub`
(define-syntax my-arith
(syntax-rules (add sub)
[(my-arith add x y)
(+ x y)] ; When `add` is encountered, replace with (+ x y)
[(my-arith sub x y)
(- x y)] ; When `sub` is encountered, replace with (- x y)
[(my-arith op x y)
(error "Unknown operation: " op)])) ; Handle other operations
The literals add
and sub
, specified in the parentheses after syntax-rules
are a list of identifiers that should be treated as literals in the pattern matching. These literals are not replaced by the macro but must match exactly in the macro usage.
Scheme macros are hygienic, this means that symbols used in their definitions are actually replaced with special symbols not used anywhere else in the program, so it is impossible to have name clashes when the macro is expanded.
A continuation is a value that encapsulates a piece of an expression’s evaluation context.
call-with-current-continuation
or call/cc
captures the current continuation starting outside the current function call and running up to the nearest enclosing prompt.
For example, in (+ 1 (+ 1 (+ 1 0)))
, at the point where 0 is evaluated, the expression context includes three nested addition expressions. We can grab that context by changing 0 to grab the continuation before returning 0.
(define saved-k #f) ; dummy var
(define (save-it!)
(call-with-current-continuation
(lambda (k) ; k is the captured continuation
(set! saved-k k) ; we save it in saved-k
0)))
(+ 1 (+ 1 (+ 1 (save-it!)))) ; => 3
In Scheme and Racket, the order of evaluation of function arguments is unspecified. This means that if you have a function call like (f a b c)
, the interpreter could choose to evaluate a
, b
, and c
in any order.
However, within the body of a function or a lambda expression, expressions are evaluated in order from top to bottom, and the value of the last expression is returned as the function's result. This is known as the "value of a procedure". This is why in the lambda body the last evaluated expression is 0
and therefore it's the return value.
The continuation saved in save-k
encapsulates the program context (+ 1 (+ 1 (+ 1 ?)))
, where ?
represents a place to plug in a result value, because that was the expression context when save-it!
was called. The continuation is encapsulated so that it behaves like the function (lambda (v) (+ 1 (+ 1 (+ 1 v))))
:
(saved-k 0) ; => 3
(saved-k 10) ; => 13
(saved-k (saved-k 0)) ; => 3 ; it is not composable!
Applying the captured continuation first aborts (to the current prompt) before restoring the saved continuation, hence the inner (saved-k 0)
is the only continuation restored.
call-with-composable-continuation
is useful if we want to nest a continuation inside a larger prompt, the last example (saved-k (saved-k 0)) ; => 6
.
We could use call/cc
as an escape procedure:
(+ 3
(call/cc
(lambda (exit)
(for-each (lambda (x)
(when (negative? x)
(exit x)))
'(54 0 37 -3 245 19))
10))) ; => 0
Or we can use it to continue from a saved state multiple times:
(define saved-cont #f) ; place to save k
(define (test-cont)
(let ((x 0))
(call/cc
(lambda (k) ; k contains the continuation
(set! saved-cont k))) ; here is saved
;; this is the continuation
(set! x (+ x 1))
(display x)
(newline)))
(test-cont) ; => 1
(saved-cont) ; => 2
(define other-cont saved-cont)
(test-cont) ; => 1 (here we reset saved-cont)
(other-cont) ; => 3 (other is still going ...)
(saved-cont) ; => 2
Hygienic macros may be a problem if we want introduce new ideas, e.g., For
syntax, like:
(For i from 1 to 10
do
(displayln i)
(when (= i 5)
(break)))
The first i
will be replaced in the expansion, this means that i
in the macro definition and i
in the macro usage context are treated as distinct variables unless explicitly handled.
A simple solution using an escape procedure:
(define-syntax For
(syntax-rules (from to break: do)
((_ var from min to max break: br-sym do body ...)
(let* ((min1 min)
(max1 max)
(inc (if (< min1 max1) + -)))
(call/cc (lambda (br-sym)
(let loop ((var min1))
body ...
(unless (= var max1)
(loop (inc var 1))))))))))
(For i from 1 to 10 break: get-out
do (display i)
(when (= i 5)
(get-out))) ; => 12345
call/cc
captures the current continuation (essentially, the current point in the program execution) and binds it to br-sym
, allowing the loop to be exited prematurely when br-sym
is called: when you call get-out
within the loop, you're effectively invoking the continuation that was captured by call/cc
. This continuation represents the state right after the call/cc
call but before entering the loop.
Invoking this continuation with get-out
"jumps" out of the loop and resumes execution from that point, effectively bypassing any further loop iterations and any code following the invocation of get-out
within the loop's body.
In this case the "escape sequence" (or break symbol) is in the body of the macro, the call/cc is used for its abort effect when it is called, which happens when in the body get-out
is evaluated. If we had used call-with-composable-continuation
we would cycle through the entire range.
(define-syntax For
(syntax-rules (from to break: do)
((_ var from min to max break: br-sym do body ...)
(let* ((min1 min)
(max1 max)
(inc (if (< min1 max1) + -)))
(call-with-composable-continuation (lambda (br-sym)
(let loop ((var min1))
body ...
(unless (= var max1)
(loop (inc var 1))))))))))
(For i from 1 to 10 break: get-out
do (display i)
(when (= i 5)
(get-out))) ; => 12345678910
We define min1
and max1
with let*
because they could be any (expensive) expression, so we want to force the evaluation once. Moreover, if min
or max
have side-effects (e.g. iterators) each expansion would end in a different value, leading to undesired results.
While let
itself doesn't force evaluation, the sequential nature of let*
does cause min
and max
to be evaluated in this context.
Scheme standards provide already exception handling but, for the sake of learning we are going to create a simple exception system ourselves.
The first thing we need is a stack for installed handlers:
(define *handlers* (list)) ; list == '()
(define (push-handler proc)
(set! *handlers* (cons proc *handlers*)))
(define (pop-handler)
(let ((h (car *handlers*)))
(set! *handlers* (cdr *handlers*)) h))
throw
: if there's a handler, we pop it and call it, otherwise we raise an error:
(define (throw x)
(if (pair? *handlers*) ; same as (not (eqv? '() *handlers*))
((pop-handler) x)
(apply error x)))
error
is an already defined procedure in Scheme, it takes a string as a mandatory first argument, which is the error message, and optional additional arguments, which are included in the error report.
(apply error x)
applies the error
function to the elements in the list x
. If x
is a list of strings, the first string will be used as the error message, and the rest of the strings will be included in the error report.
try-catch
macro:
(define-syntax try
(syntax-rules (catch)
((_ exp1 ...
(catch what hand ...))
(call/cc (lambda (exit) ; install the handler
(push-handler (lambda (x)
(if (equal? x what)
(exit
(begin
hand ...))
(throw x))))
(let ((res ;; evaluate the body
(begin exp1 ...)))
; ok: discard the handler
(pop-handler)
res))))))
Example:
(define (foo x)
(display x) (newline)
(throw "hello"))
(try
(display "Before foo ")
(newline)
(foo "hi!")
(display "After foo") ; unreached code
(catch "hello" ; this is the handler block
(display "I caught a throw.") (newline)
#f))
;; => Before foo
;; hi!
;; I caught a throw.
;; #f
A class is a definition of a type (set).
An object is a value of a type (element of a set).
Defining classes with closures in Scheme is the only way of having local data, which can be accessed by methods:
;; class
(define (make-simple-object)
(let ((my-var 0)) ; attribute
;; methods:
(define (my-add x)
(set! my-var (+ my-var x)) my-var)
(define (get-my-var) my-var)
(define (my-display)
(newline)
(display "my Var is: ")
(display my-var)
(newline))
(lambda (message . args)
(apply (case message
((my-add) my-add)
((my-display) my-display)
((get-my-var) get-my-var)
(else (error "Unknown Method!")))
args))))
(define a ( make-simple-object ))
(define b ( make-simple-object ))
(a 'my-add 3) ; => 3
(a 'my-add 4) ; => 7
(a 'get-my-var) ; => 7
(b 'get-my-var) ; => 0
(a 'my-display) ; => My Var is: 7
Even if some methods do not have arguments, we still need to add parentheses around the method name when defining them. This is because the method name is treated as a function call, and the parentheses are required to indicate that it's a function call with no arguments. If we omit the parentheses, the method name would be treated as a variable reference, which would cause a different behavior.
For example, (define (my-display) ...)
defines a function named my-display
, while (define my-display ...)
would define a variable named my-display
with the value of the following expression, that means it will evaluate the expression as soon as possible (and bind the result to the variable) instead of making it a callable function.
Inheritance is code reuse, which is desirable, how can we implement it?
Inheritance by delegation:
(define (make-son)
(let ((parent (make-simple-object)) ; inheritance
(name "an object"))
(define (hello) "hi!")
(define (my-display)
(display "My name is ")
(display name)
(display " and")
(parent 'my-display))
(lambda (message . args)
(case message
((hello) (apply hello args ))
;; overriding
((my-display) (apply my-display args))
;; parent needed
(else (apply parent (cons message args)))))))
(define c (make-son))
(c 'my-add 2)
(c 'my-display); => My name is an object and
; my Var is: 2
(display (c 'hello )) ; => hi!
Prototype-based object orientation:
Keys are symbols, either of an attribute (data) or method (function).
An object is implemented with a hash table:
(define new-object make-hash)
(define clone hash-copy)
make-hash
takes a list of pairs and creates an hash table.
hash-copy
takes an hash table and returns a mutable hash table with the same mappings.
hash-set
takes an hash table, a key and a value, it maps each key to each value in the hash table.
hash-ref
is a function used to retrieve a value from a hash table given a specific key. If the key is present in the hash table, hash-ref
returns the associated value. Here's the basic usage:
(hash-ref hash-table key [failure-result])
hash-table
is the hash that you want to look up.key
is the key for which you want to find the corresponding value.failure-result
is an optional argument. If provided and the key is not found in the hash, hash-ref
will return this value instead of raising an error.Here's an example:
; Create a hash table
(define my-hash (make-hash 'a 1 'b 2 'c 3))
; Retrieve value associated with key 'a
(define value-a (hash-ref my-hash 'a))
(displayln value-a) ; Output: 1
; Attempt to retrieve value with key 'd', which does not exist in the hash
(define value-d (hash-ref my-hash 'd "Key not found"))
(displayln value-d) ; Output: "Key not found"
Object methods can be implemented using macros: we can avoid quoting the keys.
(define-syntax !! ;; setter
(syntax-rules ()
((_ object msg new-val)
(hash-set! object 'msg new-val))))
(define-syntax ?? ;; reader
(syntax-rules ()
((_ object msg)
(hash-ref object 'msg))))
(define-syntax -> ;; send message
(syntax-rules ()
((_ object msg arg ...)
((hash-ref object 'msg) object arg ...))))
First, we define an object and its methods:
(define new-object make-hash)
(define clone hash-copy)
(define Pino (new-object))
(!! Pino name "Pino") ;; slot added
(!! Pino hello
(lambda (self x) ;; method added
(display (?? self name ))
(display ": hi, ")
(display (?? x name ))
(display "!")
(newline)))
(!! Pino set-name
(lambda (self x)
(!! self name x)))
(!! Pino set-name-&-age
(lambda (self n a)
(!! self name n)
(!! self age a)))
And a clone:
(define Pina (clone Pino))
(!! Pina name "Pina")
Using the example:
(-> Pino hello Pina) ; Pino: hi , Pina!
(-> Pino set-name "Ugo")
(-> Pina set-name-&-age "Lucia" 25)
(-> Pino hello Pina) ; Ugo: hi , Lucia!
Inheritance is not typical of prototype object systems, still is implemented as inheritance by delegation:
(define (deep-clone obj)
(if (not (hash-ref obj '<<parent>> #f))
(clone obj)
(let* ((cl (clone obj))
(par (?? cl <<parent>>)))
(!! cl <<parent>> (deep-clone par)))))
(define (son-of parent)
(let ((o (new-object )))
(!! o <<parent>> (deep-clone parent ))
o))
Deep cloning clones also the ancestors of the object:
(hash-ref obj '<<parent>> #f)
in this case we provide a standard default value #f
in the key is not present in the hash tableBasic dispatching:
(define (dispatch object msg)
(if (eq? object 'unknown)
(error "Unknown message" msg)
(let ((slot (hash-ref
object msg 'unknown)))
(if (eq? slot 'unknown)
(dispatch (hash-ref object
'<<parent>>
'unknown) msg)
slot))))
This dispatching mechanism is a fundamental part of implementing a prototype-based object system in Scheme. It allows objects to inherit behavior from their prototypes (parents) and supports polymorphism, as the same message can be handled differently by different objects depending on their specific implementations or the implementations of their prototypes.
We now have to modify ??
and ->
for dispatching:
(define-syntax ?? ;; reader
(syntax-rules ()
((_ object msg)
(dispatch object 'msg))))
(define-syntax -> ;; send message
(syntax-rules ()
((_ object msg arg ...)
(( dispatch object 'msg) object arg ...))))
Example:
(define Glenn (son-of Pino))
(!! Glenn name "Glenn")
(!! Glenn age 50)
(!! Glenn get-older
(lambda (self)
(!! self age (+ 1 (?? self age )))))
(-> Glenn hello Pina) ; Glenn: hi , Lucia!
(-> Glenn ciao) ; error: Unknown message
(-> Glenn get-older) ; Glenn is now 51