Macros are always touted as a flagship feature of scheme, the ultimate tool in the tool-belt. That said, I have a tendency to only learn things as I come across a need for them and hadn't reached for macros much until now.
What are macros?
Macros are scheme's ability to make new syntax for itself. That might not be the best definition for it, but many other resources for macros provide 'science-y' definitions for it (where I'm often left wondering, 'cool, but what are macros?') so I thought I would try a more succinct characterization. With that in mind, lets now consider the more formal definition: syntax goes into a macro, it undergoes a syntax transformation, and then new syntax is output from the macro.
The first macro I truly interacted with 'in the wild' was because I was curious of how chez-gl worked. In it, the following macro is defined:
(define-syntax define-function (syntax-rules () ((_ ret name args) (define name (foreign-procedure (symbol->string 'name) args ret)))))
Therefore, if (define-function void glClearIndex (float)) is called, then it results in the following expansion:
(define glClearIndex (foreign-procedure "glClearIndex" (float) void))
This was the perfect, gentle introduction to macros. I knew the end goal was to call (foreign-procedure '
This macro saved the need to name the definition as well as the foreign-procedure, also allowing it to be passed without string quotes. This macro mapped the input to a structure that more closely resembles C: return type first, then name, then arguments. I decided that I'd prefer, in my FFI adventures, if the name wasn't the same as the foreign procedure name. I prefer names-to-look-like-this in Scheme over their camelCased counterparts in C. It was straightforward enough to modify that macro to allow passing of a name and it's foreign procedure counterpart:
(define-syntax define-function (syntax-rules () ((_ ret name fpname args) (define name (foreign-procedure (symbol->string 'fpname) args ret)))))
So with that ready, I began wrapping IUP for Chez. IUP is a cross-platform GUI library and as such some of their components are nestable. Generally, these are created with variadic functions that accept other components as children and are are ultimately passed a null byte (0) to indicate the end of the list of children :
dlg = IupDialog(IupVbox(label, otherLabel, NULL));
This proved to be the first major roadblock.
Working with Variadic Foreign Procedures
It's not directly possible to use the aforementioned foreign-procedure syntax with variadic functions. The signature of the arguments has to be defined. But what if, on the fly, the call could generate the right underlying syntax based on the number of arguments passed?
Stack overflow to the rescue!
Luckily, someone on Stack Overflow had this same issue. I was pleasantly surprised to see the answer reflect my prediction on how to approach it, especially since my extent of macro knowledge was 'I'm pretty sure that's possible but I have no clue how.'
My iup-vbox code ended up looking like this:
(define create-list (lambda (element n) "create a list by replicating element n times" (letrec ((helper (lambda (lst element n) (cond ((zero? n) lst) (else (helper (cons element lst) element (- n 1))))))) (helper '() element n)))) (define-syntax iup-vbox (lambda (x) (syntax-case x () ((_ cmd ...) (with-syntax ((system-call-spec (syntax (create-list 'void* (length (syntax (cmd ...))))))) (with-syntax ((proc (syntax (eval `(foreign-procedure "IupVbox" (,@system-call-spec) void*))))) (syntax (proc cmd ...))))))))
This had some issues though, the first issue was fun and simple to tackle. My vbox function had to be called with a null byte (0) at the end: (iup-vbox my-label my-other-label 0). I wasn't wild about this, if it needs to end with 0, why not just append that to the list and simply list the children as arguments? Inside the lambda, I simple append 0 to the incoming syntax (also as syntax).
(let ([new-x (append (syntax->list x) (list (syntax 0)))]) (syntax-case new-x () ((_ cmd ...)
The remaining issue was far more daunting. If I wanted hbox, or any of the other container components, I'd be copying that whole body for each one...
I heard you like macros so I put macros in your macros.
Here I was, trying to dip my toe into the world of macros, and within the hour I find myself needing to write macros that generate macros. But I knew I wanted to be able to set up containers like this: (define-ntv-function void* iup-vbox IupVbox) (ntv-function meaning, 'null-terminated variadic function').
But with the template already defined, it was surprisingly straight forward.
(define-syntax define-ntv-function (lambda (x) (syntax-case x () ((_ ret name fpname) (syntax (define-syntax name (lambda (x) (let ([new-x (append (syntax->list x) (list (syntax 0)))]) (syntax-case new-x () ((_ cmd (... ...)) (with-syntax ((system-call-spec (syntax (create-list 'void* (length (syntax (cmd (... ...)))))))) (with-syntax ((proc (syntax (eval `(foreign-procedure (symbol->string 'fpname) (,@system-call-spec) ret))))) (syntax (proc cmd (... ...)))))))))))))))
The biggest challenge here was the (... ...) pattern seen in the nested macro. This is the syntax for escaping ... and allowing the inner ... to refer to the proper context. I was quite stuck on this but thankfully I came across that syntax in 'Programming With Hygienic Macros' just as @rocketnia on Discord posted about it as well, finally unblocking me from the exception: extra ellipsis in syntax form error that was plaguing me previously.
Finally, I could simply create container components with a concise syntax:
(define-ntv-function void* iup-vbox IupVbox) (define-ntv-function void* iup-hbox IupHbox)
As 'over-my-head' as some of that felt while going through it, looking back at it and jotting it down, it seems so straightforward now. I'm looking forward to digging deeper with macros, knowing I've only just gotten started.