From ad07bc268f348d4679cb1dd239ddf03a9e7c913a Mon Sep 17 00:00:00 2001 From: Lucian Mogosanu Date: Sat, 6 Jul 2019 13:18:42 +0300 Subject: [PATCH] posts, 095: Add likbez on cl-who, macros etc. http://btcbase.org/log/2019-07-06#1921946 --- posts/y05/095-cl-who-ii.markdown | 176 +++++++++++++++++++++++++++++++++++++- 1 file changed, 175 insertions(+), 1 deletion(-) diff --git a/posts/y05/095-cl-who-ii.markdown b/posts/y05/095-cl-who-ii.markdown index 24f9734..b7a7287 100644 --- a/posts/y05/095-cl-who-ii.markdown +++ b/posts/y05/095-cl-who-ii.markdown @@ -78,7 +78,7 @@ as-is. However, this S-expression now also contains the definition of `htm` in its macro-scope, which causes further occurences of `htm` to be expanded, which allows nested HTML-in-Lisp-in-HTML-... expressions. -To put this convoluted[^3] explanation in simpler words: +To put this convoluted[^3] explanation in simpler[^4] words: `with-html-output` throws us into a HTML template context; `loop` (or some other CL control structure) gets us out of there, but `htm` brings us back in, which is precisely the "mix HTML and CL" machinery @@ -218,6 +218,178 @@ rough around the edges, but don't hesitate to play with it. down, which exercise unfortunately goes way beyond the scope of this humble article. +[^4]: **Update**, July 6: The esteemed readers inform me that I'm all + over [the place][btcbase-1921771] with my attempt at helping them + make sense of this, and I doubt that my [second + attempt][btcbase-1921902] is helping much either. So let's take a + step back and do a third attempt. + + Take any arbitrary (Common) Lisp function `f`, that we've just + finished writing. We naturally want to execute this new function. + In order to do that, the function gets compiled and then the + resulting code gets evaluated. In fact there's much more happening + there, but I'm trying to get the general readership familiar with + this, so bear with me, mkay? + + So in our scenario, before the compilation phase, one of the steps + involves taking the expressions used within `f` that were + previously defined as "CL macros" and macroexpanding them. That + is, there is, potentially, code in `f` that gets executed *at + compile-time*. Most of the time we're doing this because a. we + want to preserve code modularity; while b. not fucking up run-time + performance; but there's a deeper consideration to be had in mind, + namely that CL macros allow the user to define "sub-languages" + that, when employed correctly, will result in code that fits in + head. + + Let us, for example, say that we wanted to output a HTML page that + contained "Hello, \$name!", where "\$name" is to be replaced with + a user-provided variable. With nothing but our bare-bones Lisp, we + would write something along the lines of: + + ~~~~ {.commonlisp} +(defun f (name stream) + (write-string "Hello, " stream) + (write-string name stream) + (write-string "!")) +~~~~ + + and then we'd say, e.g.: + + ~~~~ +> (f "spyked" *standard-output*) +Hello, spyked! +~~~~ + + which does precisely what we want, only this quickly gets uglier + with the amount of content we put in our HTML page. So one thing + we could do is to wrap some of our previous code into macros: + + ~~~~ {.commonlisp} +(defmacro html-body (stream) + `(write-string "" ,stream)) +;; +(defmacro hello-to (name stream) + `(progn + (write-string "Hello, " ,stream) + (write-string ,name ,stream) + (write-string "!" ,stream))) +;; +(defmacro /body-/html (stream) + `(write-string "" ,stream)) +~~~~ + + Notice how the macro definitions wrap code that we're *not* + executing at compile time into backquotes and commas, which form a + syntactic mechanism for controlling evaluation. This footnote is + becoming a blogpost of its own, so I won't get into further + details on the particular topic. + + Getting back to our `f`, its definition becomes: + + ~~~~ {.commonlisp} +(defun f (name stream) + (html-body stream) + (hello-to name stream) + (/body-/html stream)) +~~~~ + + which looks a lot better than the previous, but does the same + thing. Say we wanted to look at the macro expansion process by + ourselves. Then we would do: + + ~~~~ {.commonlisp} +> (macroexpand-1 '(hello-to "spyked" *standard-output*)) +(PROGN + (WRITE-STRING "Hello, " *STANDARD-OUTPUT*) + (WRITE-STRING "spyked" *STANDARD-OUTPUT*) + (WRITE-STRING "!" *STANDARD-OUTPUT*)) +~~~~ + + What CL-WHO does is to generalize this HTML-outputting mechanism + into its own templating language. For the sake of illustrating how + growing complexity is handled, we can rewrite a variation on the + example above in CL-WHO as: + + ~~~~ {.commonlisp} + (defun f (name stream) + (cl-who:with-html-output (stream) + (:html (:body + (cl-who:str "Hello, ") + (if (string= name "spyked") + (cl-who:htm (:b (cl-who:str "my man"))) + (cl-who:str name)) + (cl-who:str "!"))))) +~~~~ + + which works like: + + ~~~~ {.commonlisp} +> (f "gigi" *standard-output*) +Hello, gigi! +> (f "spyked" *standard-output*) +Hello, my man! +~~~~ + + and expands to: + + ~~~~ {.commonlisp} + > (macroexpand-1 + '(cl-who:with-html-output (stream) + (:html (:body + (cl-who:str "Hello, ") + (if (string= name "spyked") + (cl-who:htm (:b (cl-who:str "my man"))) + (cl-who:str name)) + (cl-who:str "!"))))) +;; The result: +(LET ((STREAM STREAM)) + (CHECK-TYPE STREAM STREAM) + (MACROLET ((CL-WHO:HTM (&BODY CL-WHO::BODY) + `(CL-WHO:WITH-HTML-OUTPUT (,'STREAM NIL :PROLOGUE NIL :INDENT + ,NIL) + ,@CL-WHO::BODY)) + (CL-WHO:FMT (&REST CL-WHO::ARGS) + `(FORMAT ,'STREAM ,@CL-WHO::ARGS)) + (CL-WHO:ESC (CL-WHO::THING) + (CL-WHO::WITH-UNIQUE-NAMES (CL-WHO::RESULT) + `(LET ((,CL-WHO::RESULT ,CL-WHO::THING)) + (WHEN ,CL-WHO::RESULT + (WRITE-STRING (CL-WHO:ESCAPE-STRING ,CL-WHO::RESULT) + ,'STREAM))))) + (CL-WHO:STR (CL-WHO::THING) + (CL-WHO::WITH-UNIQUE-NAMES (CL-WHO::RESULT) + `(LET ((,CL-WHO::RESULT ,CL-WHO::THING)) + (WHEN ,CL-WHO::RESULT (PRINC ,CL-WHO::RESULT ,'STREAM)))))) + (WRITE-STRING "" STREAM) + (LET ((CL-WHO::*INDENT* NIL)) + NIL + (CL-WHO:STR "Hello, ")) + (LET ((CL-WHO::*INDENT* NIL)) + NIL + (IF (STRING= NAME "spyked") + (CL-WHO:HTM (:B (CL-WHO:STR "my man"))) + (CL-WHO:STR NAME))) + (LET ((CL-WHO::*INDENT* NIL)) + NIL + (CL-WHO:STR "!")) + (WRITE-STRING "" STREAM))) +~~~~ + + Maybe not the most beautiful piece of code, but it all becomes + clear once you become familiar with the CL-WHO implementation. + + So, to summarize: CL-WHO is a compiler that parses a templating + language that represents HTML nodes as Lisp lists starting with + keywords, e.g. ":em" is the representation of "<em>"; but + the same language allows us to express mechanical tasks, + e.g. looping over a list of items, which requires a special symbol + (`cl-who:htm`) to move us back in the "HTML page" context. The + *result* of a "with-html-output" code is a program that outputs + HTML to wherever we want. + + So then, I guess that makes CL-WHO a HTML generator generator? + [btcbase-1919627]: http://btcbase.org/log/2019-06-23#1919627 [hunchentoot-i]: /posts/y05/093-hunchentoot-i.html [btcbase-1919634]: http://btcbase.org/log/2019-06-23#1919634 @@ -229,3 +401,5 @@ rough around the edges, but don't hesitate to play with it. [cl-who-syntax]: http://archive.is/3kH5V#selection-835.0-835.20 [cl-who-demo]: /uploads/2019/07/cl-who-demo/ [coad]: http://coad.thetarpit.org/ +[btcbase-1921771]: http://btcbase.org/log/2019-07-05#1921771 +[btcbase-1921902]: http://btcbase.org/log/2019-07-06#1921902 -- 1.7.10.4