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-formatformat to display data records.
Common Lisp format and Elisp format are very different from each other.
See A Light Introduction to formatting strings in Elisp
for a discussion of this and some links to resources describing Common Lisp format.format is relatively sophisticated/complicated,
while Elisp format is more or less the same as the familiar printf.
In this port of Peter Seibel's Common Lisp to Elisp;
I initially used an elisp package cl-format by Andreas Politz.
It is a good package and sufficient for the purpose.
However the installation and use of cl-format in Elisp seemed to distract
students unfamiliar with Common Lisp from the main point of Seibel's chapter --- his cool macros.
So I have written that part to use the (printf-like) Elisp 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)
;; If you have `cl-format' installed, you can use this one-liner adapted from P. Seibel
;; (cl-format (pcl/db-display-buffer) "~{~{~a:~10t~a~%~}~%~}" db-entries)
;; instead of the following dolist.
(dolist (entry db-entries)
(let ((top entry));; Note (pop TOP) will not modify ENTRY nor DB-ENTRIES
(while top
(insert (format "%-10S" (pop top))
(let ((it (pop top)))
;; For string IT, (format "%S" IT) would return "\"IT\""
;; But we don't want the "".
(if (stringp it) it (format "%S" it))
)
"\n")
))
(insert "\n")
)
(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))
)
(message nil);; Clear echo area.
;; Otherwise "Enter another..." would linger until the next key or mouse event.
)
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 a 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.