Search code examples
schemelispguilelisp-macros

Generating cond's (test expression ...) in Scheme


I've been using Guile for about a year, but am fairly inexperienced in using macros in Scheme. Although I have got some more complicated examples to work satisfactorily, I'm getting stuck on what (to me) feels like a really simple use case akin to simple substitution similar to what can be achieved with #define in C.

I have a function that uses cond to test several conditions, some which have a general-form. For example:

(define (file->list filename)
  "Read input and split into list of 
   coordinates and folds."
  (let ((lst (call-with-input-file filename
           (λ (p)
         (list-ec (:port line p read-line)
              (cond
               ((string-any (cut eqv? <> #\,) line)
                (string-split line #\,))
               ((string-null? line) #f) ;; blank line
               ((string= line "fold along x=" 1 13 1 13)
                `(x ,(last (string-split line #\=))))
               ((string= line "fold along y=" 1 13 1 13)
                `(y ,(last (string-split line #\=))))
               (else (error "bad input!"))))))))
    (parse-input lst)))

I'd like to get rid of the repetition around conditions of the form below:

((string= line "fold along x=" 1 13 1 13)
`(x ,(last (string-split line #\=))))

This feels to me like a macro because this boilerplate can be generated at compile time using pattern matching - and I have naively tried something like this:

(define-syntax-rule (fold-parse str x-or-y)
   `((string= ,str
         ,(string-append "fold along " (symbol->string x-or-y) "=")
         1 13 1 13)
      (x-or-y ,(string->number (last (string-split str #\=))))))

This does reproduce the (test expression) s-expression at the REPL:

scheme@(guile-user)> (fold-parse "fold along x=3" 'x)
$33 = ((string= "fold along x=3" "fold along x=" 1 13 1 13) ((quote x) 3))
scheme@(guile-user)> 

But when I try to slot the macro into my cond I get the following error:

;;; WARNING: compilation of /home/foo/dev/aoc_2021/13/./13.scm failed:
;;; Syntax error:
;;; /home/foo/dev/aoc_2021/13/./13.scm:53:28: source expression failed to match any pattern in form fold-parse
ice-9/psyntax.scm:2794:12: In procedure syntax-violation:
Syntax error:
unknown location: source expression failed to match any pattern in form fold-parse

I'm naively adding it just like below - I've commented out the boilerplate in the cond around the "fold along x=" boilerplate its meant to replace:

(define (file->list filename)
  "Read input and split into list of 
   coordinates and folds."
  (let ((lst (call-with-input-file filename
           (λ (p)
         (list-ec (:port line p read-line)
              (cond
               ((string-any (cut eqv? <> #\,) line)
                (string-split line #\,))
               ((string-null? line) #f) ;; blank line
               (fold-parse line 'x)
               ;;((string= line "fold along x=" 1 13 1 13)
               ;; `(x ,(last (string-split line #\=))))
               ((string= line "fold along y=" 1 13 1 13)
                `(y ,(last (string-split line #\=))))
               (else (error "bad input!"))))))))
    (parse-input lst)))

Since this attempt, I've been down a bit of rabbit-hole of syntax-case, quasisyntax, and numerous other variations on the macro and cond to try to make it work.

However, I'm obviously not getting something fundamentally important about the way macros can "drop-in replace" a snippet or part of an expression in their place.

Can anyone help me see the error of my ways?

How do I write a macro that can generate a test and expression to be used inside a cond-clause? And - is it a reasonable/sensible thing to do?


Solution

  • The cond will be expanded before your macro. Thus.

    (cond 
      ...
      (fold-parse line 'x)
      ...)
    

    Will first be turned into:

    (if ...
        (if fold-parse
            (begin line 'x)
            ...)
    

    Thus you probably get an unbound variable error or perhaps pigs fly. Anyway, how cond works is that if a cond terms only has a test the truthy value will be the result of the cond, thus you can do something like this:

    (define (handle-fold line var)
      (let ((str (string-append "fold along " (symbol->string var) "=")))
        (and (string= line str 1 13 1 13)
             (list var (last (string-split line #\=))))))
    

    And in you cond:

    (cond
      ((string-any (cut eqv? <> #\,) line)
       (string-split line #\,))
      ((string-null? line) #f) ;; blank line
      ((handle-fold line 'x))  ;; NB: The truthy return is the result
      ((handle-fold line 'y))  ;; NB: The truthy return is the result
      (else (error "bad input!"))))))))
    

    Now looking at the amount of code it doesn't really get much easier so I would have been happy with the initial version and probably started considering the alternative if the number of similar lines multiply further. Right now it might be the two lines will change and be different in a future version and the effort might be lost. It happens more often than my predictions help me in future, but I also like keeping it DRY.