OCaml has a couple of macro systems as libraries for it. The most elixir-like is called ppx_stage
or something like that, a simple staged macro system. It uses OCaml attributes so I’ll use it’s syntax here, but you could easily come up with language built-in’s instead!
Here is the jist of how it works:
Quote code
# let greeting = [%code print_string "Hello!\n"];;
val greeting : unit Ppx_stage.code = {Ppx_stage.compute = <fun>; source = <fun>}
The code
attribute takes an expression argument and returns it as a code expresesion with the type of the return value of the expression (unit
in this case, I.E. {}
in Elixir terms).
Converting quoted code at compile-time to an actual expression
The returned code can be ‘run’ at compile-time:
# Ppx_stage.run greeting;;
Hello!
- : unit = ()
The PPX looks for Ppx_stage.run
calls (or run
scoped within Ppx_stage
to be specific, so you can open it for example) taking a 'a Ppx_stage.code
type and rewrites it to be the expression represented, so the above is quite literally compiled and run as:
# (fun () -> print_string "Hello!\n") ()
- : unit = ()
Altering quoted code
You can build and splice and alter quoted code of course as well:
# let two = [%code 2];;
val two : int Ppx_stage.code = 2
# let three = [%code 1 + [%e two]];;
val three : int Ppx_stage.code = 1 + 2
The e
attribute inside a code
attribute takes the code that two
represents, so the AST value of the literal 2
, and puts it in place of itself, so three
becomes [%code 1 + 2]
.
User quoting
And the big part that lets you make an elixir-like macro is being able to accept user quotes:
let map f = [%code
let rec go = function
| [] -> []
| x :: xs -> [%e f [%code x]] :: go xs in
go]
So this makes a perfectly optimized function for the type of what is being passed in on demand, so it uses the f
from outside the code
attribute, and thus this whole map
function gets this nasty type of:
val map :
('a Ppx_stage.code -> 'b Ppx_stage.code) ->
('a list -> 'b list) Ppx_stage.code = <fun>
However, this means you have to pass quoted code ‘to’ this function, so calling it like:
map [%code [%e x] + 1]
Passing code to function calls is annoying though, so can at least make it more like ‘usual’ code, so it gives this:
map (fun%staged x -> x + 1)
So now it looks mostly like a normal function call.
Elixir does the same thing automatically by detecting if the map
function is actually a MACRO-map
function on the module first, and if so then it just calls that passing in the AST instead of compiling a call to map
, so with actual compiler support it is pretty trivial to do kind of the same thing, but instead just check type that it takes and returns and if it is a macro then just use it as such, or follows Elixir as a naming convention, whichever. 