format
, to format strings.format
functionality is available
in Elisp with cl-format
.
Note that while the Elisp and Common Lisp format
commands are similar in the broad sense; in detail they are completely different.
The elisp format
specification mainly shadows printf
from C
; while Common Lisp format
defines its own elaborate string formatting language.
format
's formidable repertoire of directives.format
in Guy Steele's Common Lisp the Language, 2nd Edition.
As for information specific to the Elisp version;
I have not seen info pages for cl-format
, but it does have an especially detailed
docstring (use describe-function to see it).
-*- lexical-binding: t; -*-
to the buffer head line.
To confirm that lexical binding is on in the buffer, you can use eval-expression
to see the value of the variable lexical-binding
.
Note that variable is buffer local so its value may differ from buffer to buffer.
cl-format
cl-lib
for function such as cl-remove-if-not
and cl-format
for an approximation of the common lisp format
function.cl-format
can be obtained from the elpa package repository.
I use use-package
to load it.use-package
, you can install it like this:
(unless (package-installed-p 'use-package) (package-refresh-contents) (package-install 'use-package))And then use
use-package
to install cl-format
:
(use-package cl-format :ensure t)So programs can require it.
(require 'cl-format)
(defun pcl/make-cd (title artist rating ripped) (list :title title :artist artist :rating rating :ripped ripped)) (defvar pcl/*db* nil) (defun pcl/add-record (cd) (push cd pcl/*db*))
I use a slash "pcl/..." to separate the prefix and the rest of the name.
An alternative (and more common) style would be using a hyphen, as in "pcl-make-cd.
I think "pcl/make-cd" is nicer because it visually separates the prefix pcl
from the rest of the name.
Note however that (require 'pcl/package)
would be
problematic, since it implies a filename containing a '/' character.
(progn (pcl/add-record (pcl/make-cd "Roses" "Kathy Mattea" 5 t)) (pcl/add-record (pcl/make-cd "Fly" "Dixie Chicks" 6 t)) (pcl/add-record (pcl/make-cd "Home" "Dixie Chicks" 7 t)) (pcl/add-record (pcl/make-cd "Give Us a Break" "Limpopo" 10 t)) (pcl/add-record (pcl/make-cd "玻璃心" "黃明志 & 陳芳語" 9 t)) )
(defvar pcl/db-display-buffer*)
dump-db
to dump the contents of the database to standard out.pcl/db-display-buffer*
(defun pcl/display-entries (db-entries) "Show contents of database DB-ENTRIES in pcl/db-display* buffer" (if (not db-entries) (message "No entries to show.") (setq pcl/db-display-buffer* (get-buffer-create "pcl/db-display-buffer*")) (pop-to-buffer pcl/db-display-buffer*) (let (buffer-read-only) ;; temporarily set buffer-read-only to nil. (erase-buffer) (cl-format pcl/db-display-buffer* "~{~{~a:~10t~a~%~}~%~}" db-entries) (goto-char (point-min)) ) (special-mode) (view-mode 1) )) (defun pcl/display-db () "Display CD database in a display buffer." (interactive) (pcl/display-entries pcl/*db*) )
interactive
declares functions to be emacs commands, which can be invoked by name after pressing M-x.
The two lines involving the major mode special-mode
and the minor mode view-mode
are so that the buffer will be displayed in a mode suitable for viewing but not editing.
So, for example, pressing the 'q
' key should invoke View-quit
instead of inserting a "q" into the buffer.
The (let (buffer-read-only)...)
block allows the encapsulated code to alter the buffer even if
the buffer was in special-mode
before entering the block.
(defun pcl/prompt-for-cd () "Prompt user for CD info: title, artist, rating, and ripped?" (pcl/make-cd (read-string "title: ") (read-string "artist: ") (read-number "rating: ") (y-or-n-p "ripped? (y/n)") )) (defun pcl/add-cds () "Add one or more CDs to database." (interactive) (pcl/add-record (pcl/prompt-for-cd)) (while (progn (sleep-for 0.2); Pause at end of record input. (y-or-n-p "Enter another cd? (y/n)")) (pcl/add-record (pcl/prompt-for-cd)) ))
prompt-read
and parse-integer
,
I use elisp read-string
, read-number
functions to read from the emacs mini-buffer.(sleep-for 0.2)
in pcl/add-cds
is worth mentioning.
This delay between the questions "Ripped?" and "Enter another cd?" is useful in the elisp version,
Without the sleep-for
delay, the second question appears instantaneously after the first,
because elisp y-or-n-p
responds immediately when the user presses the y
or n
key (without waiting for the user to hit enter).
As a user, I found that confusing. Try removing the sleep-for
to see what I mean.
If you are cut-and-pasting the code into emacs and evaluating it as your read,
try running pcl/add-cds
now to add a few of your favorite CDs.
Maybe Queen's 1975 album "A Night at the Opera" :-)
Then use pcl/prompt-for-cd
to view the results.
(defun pcl/save-db (filename) (interactive "FDatabase filename: ") (with-temp-file filename (print pcl/*db* (current-buffer)) )) (defun pcl/load-db (filename) (interactive "fDatabase filename: ") (with-temp-buffer (insert-file-contents filename) (setq pcl/*db* (read (current-buffer))) ))
(defun pcl/select-by-artist (artist) (cl-remove-if-not #'(lambda (cd) (equal (cl-getf cd :artist) artist)) pcl/*db* )) (defun pcl/select (selector-fn) (cl-remove-if-not selector-fn pcl/*db*) )
cl-
to the common lisp names remove-if-not
and getf
.#'
quote before a lambda expression is optional, so we could just as well use:
(defun pcl/select-by-artist (artist) (cl-remove-if-not (lambda (cd) (equal (cl-getf cd :artist) artist)) pcl/*db* ))And likewise for the remaining examples.
(defun pcl/artist-selector (artist) #'(lambda (cd) (equal (cl-getf cd :artist) artist)) )Note this closure over
artist
will only work under lexical binding.
Note also that pcl/select-by-artist
does not have an interactive
declaration, so as is you cannot call it as command.
One way to try out code is to use the ielm emacs command, which provides an elisp REPL (Read-Evaluate-Print-Loop) environment in
an emacs buffer.
(cl-defun pcl/where/ifs (&key title artist rating (ripped nil ripped-p)) #'(lambda (cd) (and (if title (equal (cl-getf cd :title) title) t) (if artist (equal (cl-getf cd :artist) artist) t) (if rating (equal (cl-getf cd :rating) rating) t) (if ripped-p (equal (cl-getf cd :ripped) ripped) t) )))
where
to "pcl/where/ifs
",
Finally, note the use of cl-defun
instead of plain defun
, since in elisp
plain defun
does not provide for named arguments such as :artist
or :title
.
(cl-defun pcl/update (selector-fn &key title artist rating (ripped nil ripped-p)) "Update given fields of entries in database selected by SELECTED-FN function." (setq pcl/*db* (mapcar #'(lambda (row) (when (funcall selector-fn row) (if title (setf (cl-getf row :title) title)) ;; These three if expressions follow a pattern (if artist (setf (cl-getf row :artist) artist)) ;; and could also be generated on the fly (if rating (setf (cl-getf row :rating) rating)) ;; with a macro if desired. (if ripped-p (setf (cl-getf row :ripped) ripped)) ;; This one follows a different pattern. ) row ) pcl/*db* )))
setq
instead of setf
to set the database variable.setq
more often in elisp.
(defun pcl/delete-rows (selector-fn) (setq pcl/*db* (cl-remove-if selector-fn pcl/*db*)) ) (defun pcl/make-comparison-expr (field value) `(equal (cl-getf cd ,field) ,value) )
(defun pcl/make-comparisons-list (clauses) "Return list of comparison expressions for the given clauses CLAUSES is a list of pairs :field-name value" (cl-loop while clauses collecting (pcl/make-comparison-expr (pop clauses) (pop clauses)) ))
Otherwise the only difference is cl-loop
instead of just loop
.
An alternative implementation (for either kind of lisp) would be:
(defun pcl/make-comparisons-list (clauses) (let (acc) (while clauses (push (pcl/make-comparison-expr (pop clauses) (pop clauses)) acc ) ) (reverse acc) ;; By habit. Reversing not really necessary in this case. ))
Looking at the side-by-side (pop clauses)
expressions, one can see that
the code relies on those expressions being evaluated in order.
If the second one were to be evaluated first, field and value would be reversed!
Fortunately both the common lisp standard
(in CLHS section 3.1.2.1.2.3, "Function Forms") and the Elisp info documentation
state that function call arguments are evaluated sequentially in order.
(defmacro pcl/where/macro (&rest clauses) "SQL WHERE-like function to select records in a property list based database CLAUSES is a list of pairs :field-name value " `#'(lambda (cd) (and ,@(pcl/make-comparisons-list clauses)) ))
where
given and explained by Seibel.
<
,@
> is the splice operator.
Again, see Seibel for an explanation.
(macroexpand-1 '(pcl/where/macro :title "Dark side of the moon" :ripped t))
macroexpand-1
.
where
macro in action, evaluate this expression with your desired field names and values.
For example:
(pcl/display-entries
(pcl/select (pcl/where/macro :rating 7 :ripped t :title "Home"))
)
A more emacs way would be to define an interactive command for selectively displaying entries, but I leave that as an exercise for the reader.
Code Download
I have put the elisp code presented above in a source file for download.
In particular he does a fantastic job of motivating the use of lisp macros,
providing an elegant and natural application in the macro version of his where
function.
This page gives an elisp version of his program.
Naturally, the elisp version uses buffers to do things the common lisp version would use standard in/out for,
but overall the elisp version is very close to the original common lisp - much of it in fact identical.