posts, 095: Add likbez on cl-who, macros etc.
authorLucian Mogosanu <lucian@mogosanu.ro>
Sat, 6 Jul 2019 10:18:42 +0000 (13:18 +0300)
committerLucian Mogosanu <lucian@mogosanu.ro>
Sat, 6 Jul 2019 10:19:16 +0000 (13:19 +0300)
http://btcbase.org/log/2019-07-06#1921946

posts/y05/095-cl-who-ii.markdown

index 24f9734..b7a7287 100644 (file)
@@ -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 "<html><body>Hello, " stream)
+  (write-string name stream)
+  (write-string "!</body></html>"))
+~~~~
+
+       and then we'd say, e.g.:
+
+       ~~~~
+> (f "spyked" *standard-output*)
+<html><body>Hello, spyked!</body></html>
+~~~~
+
+       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 "<html><body>" ,stream))
+;;
+(defmacro hello-to (name stream)
+  `(progn
+            (write-string "Hello, " ,stream)
+         (write-string ,name ,stream)
+         (write-string "!" ,stream)))
+;;
+(defmacro /body-/html (stream)
+  `(write-string "</body></html>" ,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*)
+<html><body>Hello, gigi!</body></html>
+> (f "spyked" *standard-output*)
+<html><body>Hello, <b>my man</b>!</body></html>
+~~~~
+
+       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 "<html><body>" 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 "</body></html>" 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 "&lt;em&gt;"; 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