lineva
2022-11-07
Linear evaluation macro system
1Linear Evaluation Macro System
1.1Introduction
(defun read-first-line (file-name &optional required)
(la:leva
(:check-type (file-name (or (array character *) pathname)))
(:let (stream (open file-name)))
(:defer (close stream))
(:let (first-line (read-line stream nil)))
(:return first-line :if (or (not required) first-line))
(error "File ~A is empty" file-name)))
;; Expand to
(defun read-first-line (file-name &optional required)
(progn
(check-type file-name (or (array character *) pathname))
(let ((stream (open file-name)))
(unwind-protect
(let ((first-line (read-line stream nil)))
(if (or (not required) first-line)
first-line
(error "File ~A is empty" file-name)))
(close stream)))))
Although S-exp is simple, consistent and unambiguous, we do need non-S-exp feature in our code sometime. For example, in most programming languages like C, we can easily write
char* file_name = get_file_name(stdin);
assert(file_name != NULL);
FILE* handle = open_file(file_name);
assert(handle != NULL);
Error* error = process(handle);
if (error != NULL) return FLAG_ERROR;
return FLAG_OK;
However, in Lisp, we have to write following code without defining macro:
(let ((file-name (get-file-name stdin)))
(assert file-name)
(let ((handle (open-file file-name)))
(assert handle)
(let ((err (process handle)))
(if err
:error
:ok))))
Although the logic is simple, as the indentation increase and the forms nest with others, the code become somewhat complex. This package provides a C-like sequential calculation.
(la:leva
(:let (file-name (get-file-name stdin)))
(assert file-name)
(:let (handle (open-file file-name)))
(assert handle)
(:let (err (process handle)))
(if err :error :ok))
Another example is local functions in Lisp. In Scheme, we use define
to define a local function when the body of function is overlong,
rather than let
:
(define (sort lst)
(define (insert elt lst)
(cond [(null? lst) (list elt)]
[(< elt (car lst)) (cons elt lst)]
[else (cons (car lst)
(insert elt (cdr lst)))]))
(if (null? lst)
lst
(insert (car lst)
(sort (cdr lst)))))
However, in Common Lisp, since defun
always defines a global function
instead of a local function, labels
and flet
are widely used. But
the code can become cumbersome quickly as the local functions get
longer. Therefore, a Scheme-like feature for defining local functions
is available in this package:
(defun sort (lst)
(la:leva
(:defun insert (elt lst)
...)
(if (null lst) ...)))
As we saw, leva
(stand for Linearly EVAluate) is the main operator
of this package. It is generally a superset of progn
: evaluate forms
sequentially and treat forms led by keyword
specially. Therefore,
the title "Linear Evaluation Macro System" does not mean another
evaluation rule or a replacement of current Lisp macro system, but a
complement of it.
1.2Installation
Since leva
does not depend on other libraries, you can simply download
lineva.lisp and load
it. To enable leva
cooperate with a bigger
system, clone
this repository and load lineva.asd by asdf. For
example, the simplest way is that define your own system and make it
depend on lineva
:
;; foo.asd
(require :asdf)
(in-package :asdf-user)
(push "/path/to/lineva.lisp/" asdf:*central-registry*)
(defsystem :foo
:depends-on (:lineva)
:components ())
Check document of asdf for more details.
1.3Use leva
leva
is the main macro of this package, it is generally a
progn
. However, forms in top level of body of leva
will be treat
as a instruction. Formally, leva
take any number of forms and each
form have one of following shape:
(:name . LAMBDA-LIST)
:name
, which is a equivalent of(:name)
- Any Lisp form except
1.
and2.
following forms are legal parameter of leva
:
(:break "Bang!")
:break
(format t "See you space cb")
Incidentally, :break
is a built-in instruction corresponding to
standard function break
in Common Lisp. Note that a instruction is
always a keyword
, which allows leva
distinguish normal lisp form
and instructions. Instructions is valid only in top level of
leva
. So the following code is invalid:
(la:leva
(if (null lst)
(:return lst)))
1.4Create a Instruction
A instruction generally works like macro, except
- only works in context of
leva
- take rest part of evaluation as a parameter
The "rest part of evaluation" means code expanded from parameters of
leva
which follows current instruction. Take the following code for
example:
(la:leva
(:let (x 1))
(:let (y 2))
(+ x y))
For instruction invocation (:let (y 2))
, (+ x y)
is its "rest
code"; for invocation (:let (x 1))
, code expanded from
(:let (y 2))
is its "rest code". Normally, instruction should
not ignore its "rest code".
Instructions are defined by definst
, which is basically a equivalent
of defmacro
except a built-in variable $rest-code
is visible in
body of definition. For example, to define instruction let
, we can
write:
(definst :let (&rest let-arguments)
"Define local variables by `let'."
`(let ,let-arguments ,$rest-code))
The first parameter is always a keyword
. The second parameter is a
lambda-list, which correspond to cdr
part of instruction's
invocation. Rest parameter is the macro body, which generates code
like macro by implicit parameter $rest-code
. By convention, if the
first component of body is a literal string, it will be interpreted as
a docstring of this instruction.
1.5Built-in Instructions
A number of instructions have been defined. Available instructions can
be found by (la:available-instructions)
; detail usage of the
instruction can be found by (la:describe-instruction :instruction)
.
1.5.1Local Variables
1.5.1.1:let
lambda-list: :LET (&REST LET-ARGUMENTS)
Define local variables by `let'. LET-ARGUMENTS has the same meaning of `let'.
(la:leva
(:let (x 10) (y 20))
(+ x y))
1.5.1.2:let-assert
lambda-list: :LET-ASSERT (&REST LET-ARGUMENTS)
Define local variables by `let' and assert its value. LET-ARGUMENTS has the same meaning of `let'.
(la:leva
(:let-assert (x 10) (y 20) (z nil))
(+ x y z))
1.5.1.3:flet
lambda-list: :FLET (&REST FLET-ARGUMENTS)
Define local function by `flet', FLET-ARGUMENTS has the same meaning with `flet'.
(la:leva
(:flet (add1 (x) (+ 1 x))
(dot2 (x) (* 2 x)))
(dot2 (add1 10)))
1.5.1.4:labels
lambda-list: :LABELS (&REST LABELS-ARGUMENTS)
Define local function by `labels'. LABELS-ARGUMENTS has the same meaning with `labels'
(la:leva
(:labels (fib (n)
(if (< n 2)
1
(+ (fib (- n 1)) (fib (- n 2))))))
(fib 5))
1.5.1.5:macrolet
lambda-list: :MACROLET (&REST MACROLET-ARGUMENTS)
Define local macro by `macrolet'. MACROLET-ARGUMENTS has the same meaning with `macrolet'.
(la:leva
(:macrolet (record (&rest values) `(list ,@values)))
(record "Joe" 20 nil))
1.5.1.6:symbol-macrolet
lambda-list: :SYMBOL-MACROLET (&REST SYMBOL-MACROLET-ARGUMENTS)
Define a local symbol-macro by `symbol-macrolet'.
SYMBOL-MACROLET-ARGUMENTS has the same meaning with`symbol-macrolet'.(la:leva (:symbol-macrolet (x (format t "...~%")))
(list x x x))
1.5.1.7:defun
lambda-list: :DEFUN (NAME LAMBDA-LIST &BODY BODY)
Define a local function by `labels'.
(la:leva
(:defun fac (n)
(if (zerop n)
1
(* n (fac (- n 1)))))
(fac 3))
1.5.1.8:defvar
lambda-list: :DEFVAR (NAME &OPTIONAL VALUE)
Define a local variable by `let'.
(la:leva
(:defvar x 10)
x)
1.5.1.9:bind
lambda-list: :BIND (LAMBDA-LIST EXPRESSION)
Define local variables by `destructuring-bind'.
(la:leva
(:bind (a b &rest c) '(1 2 3 4 5))
(list a b c))
1.5.1.10:setf
lambda-list: :SETF (PLACE VALUE &KEY (IF T))
Invoke `setf' to set PLACE to VALUE if IF is not `nil'.
(la:leva
(:defvar name :alexandria)
(:setf name (symbol-name name)
:if (not (stringp name)))
name)
1.5.2Debug
1.5.2.1:break
lambda-list: :BREAK (&OPTIONAL FORMAT-CONTROL &REST FORMAT-ARGUMENTS)
Enter debugger by call `break'. Arguments has the same meaning with `break'.
(la:leva
(:break "Let's ~A!!!" :burn))
1.5.2.2:inspect
lambda-list: :INSPECT (OBJECT)
Enter inspector with OBJECT.
(la:leva
(:defvar x '(:foo :bar))
(:inspect x))
1.5.2.3:assert
lambda-list: :ASSERT (&REST CONDITIONS)
Quickly assert that all CONDITIONS is true.
(la:leva
(:defvar x 10)
(:assert (numberp x) (plusp x) (evenp x))
x)
1.5.2.4:check-type
lambda-list: :CHECK-TYPE (&REST CHECK-TYPE-PARAMETERS)
Invoke `check-type' over each element of CHECK-TYPE-PARAMETERS.
(la:leva
(:let (name "Joe") (age 20))
(:check-type (name (array character *) "a string")
(age (integer 0 150)))
(list name age))
1.5.3Contro Flow
1.5.3.1:return
lambda-list: :RETURN (VALUE &KEY (IF T))
Return VALUE if condition IF is true.
(la:leva
(:defvar x (read))
(:return (- x) :if (minusp x))
x)
1.5.3.2:try
lambda-list: :TRY (&REST VALUES)
Return first value in VALUES which is not `nil'. If all VALUES is `nil', evaluate rest code.
(la:leva
(:defvar table
'(:bing "cn.bing.com"))
(:try (getf table :google)
(getf table :duckduckgo)
(getf table :bing))
"No search engine available.")
1.5.3.3:defer
lambda-list: :DEFER (&REST FORMS)
Evaluate rest codes, then evaluate FORMS sequentially. Result of rest code will be returned. Evaluation of rest code will be protected by `unwind-protect'.
(la:leva
(:defun close-conn () (format t "Bye!~%"))
(format t "Hello!~%")
(:defer (close-conn) (terpri))
(format t "[...]~%"))
1.5.4Display
1.5.4.1:printf
lambda-list: :PRINTF (FORMAT-STRING &REST ARGUMENTS)
Print content to standard output. FORMAT-STRING and ARGUMENTS have the same meaning of `format'.
(la:leva (:printf "Hello ~S!~%" :world))
1.5.4.2:println
lambda-list: :PRINTLN (THING)
Print content to standard output and add newline. Use `princ' to output.
(la:leva (:println "Hello world!"))
1.5.4.3:pn
lambda-list: :PN (THING)
Print content to standard output and add newline. Use `prin1' to output.
(la:leva (:pn "Hello world!"))