Nyquist / XLISP 2.0  -  Contents | Tutorials | Examples | Reference

Environment



Global and Lexical Binding


From the XLISP perspective, there are two kinds of bindings:

  1. Global bindings are bindings to symbols in the *obarray*.

  2. Lexical bindings are bindings in a local association list

There is a third kind of binding, 'dynamical binding', used by progv.

  Back to top


Lexical Scope


Have you ever wondered why this doesn't work:

(defun print-x ()
  (print x))  ; unbound variable X

(let ((x 'hello))
  (print-x))
error: unbound variable - X

The answer is twofold:

  1. The 'print-x' function is defined at the global top-level, where no lexical environment exists.

  2. The 'print-x' function is called inside a let form, where a lexical variable binding for 'x' exists, but 'print-x' is evaluated at the global top-level, where it was defined, so 'print-x' cannot see the lexical let binding of 'x' and signals an 'unbound variable' error.

Here is a version that seems to work:

(let ((x 'hello))

  (defun print-x ()
    (print x))

  (print-x))
HELLO
  1. The 'print-x' function is defined inside a let form.

  2. The 'print-x' function is called inside the same let form as where it was defined, so 'print-x' prints the lexical let binding of 'x'.

But here again a version that does not behave as wanted:

(let ((x 'lexical))
  (defun print-x ()
    (print x)))

(let ((x 'hello))
  (print-x))
LEXICAL
  1. The 'print-x' function is defined inside a let form.

  2. The 'print-x' function is called inside a different let form as where it was defined, so 'print-x' prints the lexical let binding of 'x' from the place where it was defined.

Somehow it seems to be important where a function was defined.


Closures


Here a Lisp function, defined inside a let form:

(let ((a 'A) (b 'B) (c 'C))

  (defun print-abc ()
    (format t ";; a = ~s, b = ~s, c = ~s~%" a b c))

  )  ; end of LET

Now 'print-abc' is called outside the let form:

> (print-abc)
;; a = A, b = B, c = C
NIL

The lexical let variables 'a', 'b', and 'c' have become a permanent part of the 'print-abc' function.

  Back to top


Lisp Objects


The following examples are based on Chapter 13 of 'Paradigms of Artificial Intelligence Programming' by Peter Norvig. The code has been ported from Common Lisp to XLISP, all examples have been tested with Nyquist 3.03 in December 2010.

The function 'new-account' creates account objects, which are implemented as closures encapsulating three variables 'name', 'balance', and 'interest-rate'. An account object also encapsulates functions to handle five messages ':withdraw', ':deposit', ':balance', ':name', and ':interest', to which the object can respond:

(defun new-account (name &optional (balance 0.0) (interest-rate 0.06))
  #'(lambda (message)
      (case message
        (:withdraw #'(lambda (amount)
                       (if (<= amount balance)
                           (setq balance (- balance amount))
                           'insufficient-funds)))
        (:deposit  #'(lambda (amount)
                       (setq balance (+ balance amount))))
        (:balance  #'(lambda () balance))
        (:name     #'(lambda () name))
        (:interest #'(lambda ()
                       (setq balance (+ balance (* interest-rate balance))))))))

An account object can only do one thing, receive a message and return the appropriate function to execute that message. This function is called the 'method' that implements the message.

The function 'get-method' finds the method that implements a message for a given object:

(defun get-method (object message)
  (funcall object message))

The function 'send-message' gets the method and applies it to a list of arguments:

(defun send-message (object message &rest args)
  (apply (get-method object message) args))

Here are some examples how it works:

> (setq a1 (new-account "My Name" 1000.0))
#<Closure...>

> (send-message a1 :name)
"My Name"

> (send-message a1 :balance)
1000.0

> (send-message a1 :withdraw 500.0)
500

> (send-message a1 :deposit 123.45)
623.45

> (send-message a1 :balance)
623.45

  Back to top


Generic Functions


The 'send-message' syntax is awkward, as it is different from normal Lisp function calling syntax, and it doesn't fit in with the other Lisp tools.

For example if we want to say:

(mapcar :balance accounts)

with 'send-message' we would have to write:

(mapcar #'(lambda (acc)
            (send-message acc :balance))
        accounts)

We could fix this problem by defining a generic function 'withdraw' like this:

(defun withdraw (object &rest args)
  (apply (get-method object :withdraw) args))

Now we can write:

(withdraw account x)

instead of:

(send-message account :withdraw x)

  Back to top


Classes


The macro 'define-class' defines a class with its associated message handling methods. It also defines a generic function for each message. Finally, it allows the programmer to make a distinction between instance variables, associated with each object, and class variables, associated with a class and shared by all members of the class.

(defmacro define-class (class ivars cvars &rest methods)
  `(let ,cvars
     (mapcar #'ensure-generic-function ',(mapcar #'first methods))
     (defun ,class ,ivars
       #'(lambda (message)
           (case message
             ,@(mapcar #'make-clause methods))))))

The 'make-clause' function translates a message from 'define-class' into a case clause.

(defun make-clause (clause)
  `(,(car clause) #'(lambda ,(cadr clause) ,@(cddr clause))))

The 'ensure-generic-function' function defines a dispatch function for a message, unless it already has been defined as one:

(defun ensure-generic-function (message)
  (unless (generic-function-p message)
    (let ((fn #'(lambda (object &rest args)
                  (apply (get-method object message) args))))
      (setf (symbol-function message) fn)
      (putprop message fn 'generic-function))))

The 'generic-function-p' function tests if a function has been defined as a generic function:

(defun generic-function-p (name)
  (and (fboundp name)
       (eq (get name 'generic-function) (symbol-function name))))

Now we can define the 'account' class with 'define-class'. We make 'interest-rate' a class variable, shared by all accounts:

(define-class account (name &optional (balance 0.0)) ((interest-rate 0.06))
  (withdraw (amount)
    (if (<= amount balance)
        (setq balance (- balance amount))
        'insufficient-funds))
  (deposit (amount)
    (setq balance (+ balance amount)))
  (balance ()
    balance)
  (name ()
    name)
  (interest ()
    (setq balance (+ balance (* interest-rate balance)))))

Macroexpansion:

(let ((interest-rate 0.06))
  (mapcar (function ensure-generic-function)
          (quote (withdraw deposit balance name interest)))
  (defun account (name &optional (balance 0))
    (function (lambda (message)
      (case message
        (withdraw (function (lambda (amount)
                              (if (<= amount balance)
                                  (setq balance (- balance amount))
                                  (quote insufficient-funds)))))
        (deposit  (function (lambda (amount)
                              (setq balance (+ balance amount)))))
        (balance  (function (lambda nil balance)))
        (name     (function (lambda nil name)))
        (interest (function (lambda nil
                              (setq balance (+ balance (* interest-rate balance)))))))))))

Here is how it works:

> (setq a2 (account "my-name" 2000.0)
#<Closure...>

> (balance a2)
2000

> (deposit a2 42.0)
2042

> (interest a2)
2164.52

  Back to top


Delegation


Here is a 'password-account' class with two message clauses:

(define-class password-account (password acc) ()
  (change-password (pass new-pass)
    (if (equal pass password)
        (setq password new-pass)
        'wrong-password))
  (t (pass &rest args)
    (if (equal pass password)
        (if args
            (apply message (cons acc args))
            (funcall message acc))
        'wrong-password)))

The definition of 'password-account' depends on some internal details of 'define-class'. It uses 't' as a catch-all clause to case and uses the dispatch variable 'message'. Usually it is not a good idea to rely on details of other code, so this will be changed below.

Here is how 'password-account' works:

> (setq a3 (password-account "secret" a2))
#<Closure...>

> (balance a3 "secret")
2164.52

> (withdraw a3 "guess" 2000.0)
WRONG-PASSWORD

> (withdraw a3 "secret" 2000.0)
164.52

Here is a 'limited-account' class, where only a limited amount of money can be withdrawn at any time:

(define-class limited-account (limit acc) ()
  (withdraw (amount)
    (if (<= amount limit)
        (withdraw acc amount)
        'over-limit))
  (t (&rest args)
    (if args
        (apply message (cons acc args))
        (funcall message acc))))

The 'withdraw' message is redefined to check if the account limit is exceeded, while the 't' clause passes all other messages unchanged:

> (setq a4 (password-account "pass"
             (limited-account 100.0
               (account "limited" 500.0)))
#<Closure...>





  Back to top


Nyquist / XLISP 2.0  -  Contents | Tutorials | Examples | Reference