Writing Macros in Lisp

One should not write Lisp macros arbitrarily (at least, I don’t think so).

I first pretend that I’m just doing symbolic manipulation of static lists.

(defun rpgtk-roll-mods (dice-pool &rest forms)
  `(thread-last dice-pool
      ,(seq-map 'rpgtk--roll-mod-dispatcher forms)))

Dispatcher:

(defun rpgtk--roll-mod-dispatcher (form)
  "Where FORM is a list"
  (if (seq-contains-p form 'die)
      (cl-case (car form)
        (filter `(seq-filter (lambda (die) ,(cdr form))))
        (reducer `(seq-reduce (lambda (acc die) ,(cdr form))))
        (adjuster `(seq-map (lambda (die) ,(cdr form))))
        (t "ouch"))
    (cl-case (car form)
      (filter `(seq-filter (lambda (die) ,(append (cdr form) '(die)))))
      (reducer `(seq-reduce (lambda (acc die) ,(append (cdr form) '(die)))))
      (adjuster `(seq-map (lambda (die) ,(append (cdr form) '(die)))))
      (t "ouch"))))

Where something like:

(rpgtk--roll-mod-dispatcher '(filter evenp))

Basic validation:

(should (equal
         (rpgtk--roll-mod-dispatcher '(adjuster + 3))
         (rpgtk--roll-mod-dispatcher '(adjuster + 3 die))))

Top-level validation:

(rpgtk-roll-mods '(1 2 3)
  '(filter evenp)
  '(adjuster + 3))

Not quite right!

(defun rpgtk-roll-mods (dice-pool &rest forms)
  (cons 'thread-last
        (cons 'dice-pool
              (seq-map 'rpgtk--roll-mod-dispatcher forms)))))

Top-level validation, again:

(rpgtk-roll-mods '(1 2 3)
  '(filter evenp)
  '(adjuster + 3))

That’s it. Now for real:

(defmacro rpgtk-roll-mods (dice-pool &rest forms)
  (cons 'thread-last
        (cons dice-pool
              (seq-map 'rpgtk--roll-mod-dispatcher forms)))))

And a basic test:

(rpgtk-roll-mods '(1 2 3)
  (filter evenp)
  (adjuster + 3))

But … I want to store the results of each transformation.

(defun thread-last-keep (initial-lst &rest forms)
  (let* ((reducer (lambda (acc form)
                    (cons (funcall form (car acc)) acc))))
    (seq-reverse
     (seq-reduce reducer forms (list initial-lst)))))


Validate:

(thread-last-keep '(1 2 3)
                  (lambda (lst) (seq-filter (lambda (d) (evenp d)) lst))
                  (lambda (lst) (seq-map (lambda (d) (+ 4 d)) lst)))

Version 2 where the lambdas are extracted:

(defun thread-last-keep (initial-lst &rest forms)
  (let* ((reducer (lambda (acc form)
                    (cons
                     `((lambda (d) ,form) (car acc))
                     acc))))
    (seq-reverse
     (seq-reduce reducer forms (list initial-lst)))))

(thread-last-keep '(1 2 3)
                  '(seq-filter (lambda (d) (evenp d)) lst)
                  '(seq-map (lambda (d) (+ 4 d)) lst))

Macro that guy:

(defun thread-last-keep (initial-lst &rest forms)
  `(seq-reverse
   (seq-reduce (lambda (acc form)
                 (cons (funcall form (car acc)) acc))
               ,forms ,(list initial-lst))))

(thread-last-keep '(1 2 3)
                  (lambda (lst) (seq-filter (lambda (d) (evenp d)) lst))
                  (lambda (lst) (seq-map (lambda (d) (+ 4 d)) lst)))
(seq-reverse
 (seq-reduce
  (lambda
    (acc form)
    (cons
     (funcall form
              (car acc))
     acc))
  ((lambda
     (lst)
     (seq-filter
      (lambda
        (d)
        (evenp d))
      lst))
   (lambda
     (lst)
     (seq-map
      (lambda
        (d)
        (+ 4 d))
      lst)))
  ((1 2 3))))

Can we get the correct results by replacing it? Let’s start with validating it from a function:

(defun rpgtk-roll-mods (dice-pool &rest forms)
  (cons 'thread-last-keep
        (cons 'dice-pool
              (seq-map 'rpgtk--roll-mod-dispatcher forms))))

(defmacro rpgtk-roll-mods (dice-pool &rest forms)
  (cons 'thread-last-keep
        (cons dice-pool
              (seq-map 'rpgtk--roll-mod-dispatcher forms))))

And the validator:

(rpgtk-roll-mods '(1 2 3)
  (filter evenp)
  (adjuster + 3))

(filter evenp) -> (seq-filter (lambda (die) (evenp die)) dice)
(adjust + 2)   -> (seq-map (lambda (die) (+ 2 die)) dice)
(reduce +)     -> (seq-reduce (lambda (acc die) (+ acc die)) dice 0)
(rpgtk--roll-mod-dispatcher '(reduce +))

(defun rpgtk-dice--roll-mod-dispatcher (form)
  "Return an expanded version of FORM.
Implements DSL for `rpgtk-dice-roll-mod'."
  (unless (seq-contains-p form 'die)
    (setq form (append form '(die))))

  (cl-case (car form)
    (filter `(seq-filter (lambda (die) ,(cdr form)) dice))
    (remove `(seq-remove (lambda (die) ,(cdr form)) dice))
    (reduce `(list (seq-reduce (lambda (acc die) ,(append (cdr form) '(acc))) dice 0)))
    (adjust `(seq-map (lambda (die) ,(cdr form)) dice))
    (t "ouch"))
  (cl-case (car form)
    (sum    `(list (seq-reduce (lambda (acc die) (+ acc die)) dice 0)))
    (-      `(seq-map (lambda (die) '(- die ,(cdr form)))) dice)
    (filter `(seq-filter (lambda (die) ,(append (cdr form) '(die))) dice))
    (remove `(seq-remove (lambda (die) ,(append (cdr form) '(die))) dice))
    (reduce `(list (seq-reduce (lambda (acc die) ,(append (cdr form) '(acc die))) dice 0)))
    (adjust `(seq-map (lambda (die) ,(append (cdr form) '(die))) dice))
    (t "ouch"))))

(defun rpgtk-dice--roll-mods (dice forms)
  (when forms
    (let ((new-dice (eval (rpgtk--roll-mod-dispatcher (car forms)))))
      (cons new-dice
            (rpgtk--roll-mods new-dice (cdr forms))))))

;; (defun rpgtk-roll-mods (dice-pool &rest forms)
;;   "Do it"
;;   (cons dice-pool
;;         (rpgtk--roll-mods dice-pool forms)))

(defmacro rpgtk-dice-roll-mod (dice-pool &rest forms)
  "Transform a list of integers, DICE-POOL, through FORMS.
Where each form can be one of the following:

  (filter func)
 "
  `(cons ,dice-pool
         (rpgtk-dice--roll-mods ,dice-pool ',forms)))

(defun rpgtk-dice-display-roll (roll-of-nums)
  "Convert ROLLS from a list of lists of integers to a string."
  (let ((roll-of-strs (seq-map 'number-to-string roll-of-nums)))
    (concat "「" (string-join roll-of-strs " ") "」")))

(defun rpgtk-dice-display (rolls)
  "Convert ROLLS from a list of lists of integers to a string.
Given:   ((3 4 1 2) (4 2) (6) (-3))
Return: 「3 4 1 2」→「4 2」→「6」→「-3」 "
  (let ((s-rolls (seq-map 'rpgtk-dice-display-roll rolls)))
    (string-join s-rolls "→")))

(rpgtk-dice-display
 (rpgtk-dice-roll-mod '(3 4 1 2)
                      (filter evenp)
                      (reduce +)
                      (adjust - 3)))

(rpgtk-dice-display
 (rpgtk-dice-roll-mod '(3 4 1 2)
                      (filter eq 0 (mod die 3))
                      (reduce +)
                      (adjust - 3)))

(mod 8 3)