Skip to content

Instantly share code, notes, and snippets.

@ejbs
Last active March 9, 2020 10:33
Show Gist options
  • Save ejbs/813fa9db68dae598064037313323f3a3 to your computer and use it in GitHub Desktop.
Save ejbs/813fa9db68dae598064037313323f3a3 to your computer and use it in GitHub Desktop.

In response to REBOL versus LISP macros

This is an attempt by me to re-create the so-called definitional binding of REBOL in LISP and is a continuation of my comment here: https://news.ycombinator.com/item?id=11587952 .

We will start by loading a couple of libraries.

(ql:quickload '( :alexandria :closer-mop))
(in-package :closer-mop)
(use-package :alexandria)

Then, we will define a dynamic environment which is where we will store the current bindings. This will be used by future methods as a default environment.

(defvar *env* ())

We will now define a class called FEXPR, inheriting from funcallable-standard-object and with the corresponding metaclass. The ENV slot is where we store the current bindings of the FEXPR, the SRC slot is there to store the current source of the FEXPR, the EFFECTIVE-FN slot simply stores the combination of these. A FEXPR is an old concept for a function which does not evaluate its arguments, in our code we will have to quote the arguments manually, this is fine since you have to do the same in REBOL. When you inherit from funcallable-standard-object and set funcallable-standard-class as your metaclass you can SET-FUNCALLABLE-INSTANCE your object with a function which will be called when your object is FUNCALLed.

(defclass fexpr (funcallable-standard-object)
  ((env :accessor env :initform nil :initarg :env)
   (src :accessor src :initform nil :initarg :src)
   (effective-fn :accessor fn :initform nil))
  (:metaclass funcallable-standard-class))

We define a MAKE-FEXPR to make it easy to create a FEXPR. The function that we use when our object gets FUNCALLed simple EVALs the EFFECTIVE-FN slot. As you can see the object shows the environment by lexically binding it outside of the body

(defun make-fexpr (src &optional (env *dynamic-env*))
 (let ((fexpr
	 (make-instance 'fexpr :env env :src src)))
    (setf (fn fexpr)
	  `(let (,@env)
	     ,(src fexpr)))
    (set-funcallable-instance-function fexpr (lambda () (eval (fn fexpr))))
    fexpr))

After we set the env or src slots we need to take care to update our effective-fn, this takes care to do that.

(defmethod (setf env) :after (nenv fexpr)
  (setf (fn fexpr)
	`(let (,@nenv)
	     ,(src fexpr))))
(defmethod (setf src) :after (nsrc fexpr)
  (setf (fn fexpr)
	`(let (,@(env fexpr))
	   ,nsrc)))

We now have a function which stores its source and environment in itself and can be dynamically changed by anyone.

Let's define a nice syntax-extension for it so we don't have to call MAKE-FEXPR manually:

(set-macro-character #\[
		     (lambda (s c)
		       (declare (ignore c))
		       `(make-fexpr ',(read-delimited-list #\] s))))
(set-macro-character #\] (get-macro-character #\) nil))

Along with a USE macro, similar to the USE function shown in the article. I wasn't sure how USE was supposed behave after mutation of a variable has happened, so I simply decided that mutation does not alter the FEXPR object's environment.

(defmacro use ((&rest vars) &body body)
  `(let ((*env* ',vars))
     ,@body))

Let's try it out with the greater than 10 example given in the article.

(defun greater10 (value code)
  (when (> value 10) (funcall code)))

(use ((msg "Hello"))
  (greater10 11 [print msg]))

In the end, I'm not sure if this is truly how Rebol works, but I still thinks it's a bit of fun so why not share it :)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment