suggestions
suggestions copied to clipboard
Macro system
Apologies if this has already been discussed elsewhere:
Are there plans to implement a macro system for gleam? If so, has any thought been put into the syntax/mechanics of such a system (i.e. more like Rust's, Elixir's, etc)?
I like the idea of some kind of macro system but I don't have any firm ideas as to what kind of system we might adopt. If you have any ideas I would be very interested!
One thing to bare in mind is that unlike Elixir we don't have the ability to execute Gleam code at compile time so a lisp style macro system is not currently possible. At a later date we may have a solution for this problem such as a web assembly backend or Gleam interpreter
Ocaml has a few different systems that you can swipe. Personally I'm partial to typed staged macro systems. ^.^
Have any links to an overview of that macro system @OvermindDL1 ? :)
First, Staged Macro's are macro's that compile to code, take code, and return code, in 'stages', like Elixir is a staged macro system, where macro's are run in stages.
Caml5p is basically the 'defacto' macro system on top of OCaml, however it's a preprocessor, thus not as type safe, not pure ocaml syntax, etc... The other patterns, the ones I prefer, are more of experiments on OCaml, like here is a PPX style staged macro implementation (not as syntactically clean as an in-language form could be, but it still shows how it works): https://github.com/stedolan/ppx_stage There are reddit and hackernews articles if you want more details that you can backlink to about that as well. But in short here's how to use it:
let greeting = [%code print_string "Hello!\n"]
Or in more modern OCaml's I'd imagine this could work (the newer ppx hoisting syntax is really nice):
let%code greeting = print_string "Hello!\n"
In Elixir this would be like making:
defmacro greeting(), do: quote(do: IO.puts("Hello!\n"))
To 'execute' it at compile-time you call it with Ppx_stage.run, so like Ppx_stage.run greeting. As the docs say, think of greeting as a function of () -> Ast.Code or something, Ppx_stage.run is watched for by the staged PPX and removed, and it then runs it's argument at compile-time, that function returns the new AST, which it then injects in place and the compiler sees if it all works there, otherwise a failure as expected.
You can have it take arguments too, here's another example from the docs, an actual real useful example:
let map f = [%code
let rec go = function
| [] -> []
| x :: xs -> [%e f [%code x]] :: go xs in
go]
So this defines a binding map, which is a function that takes one argument f, and returns whatever the PPX returns, which will basically be a real function that happens to return ast. In this case this makes a map macro that is super-fast, inlined, no generic code or indirect calls, all super fast and super efficient (think like a rust generic call). The %e ppx 'escapes' the passed in thing into the code. I.E. the above in Elixir would be like:
defmacro map(f, lst) do
# Say we are on a newer OTP and an elixir with recursive function name support
quote do
go = fn
[] -> []
[x | xs] -> [unquote(f).(x) | go xs]
end
go.(lst)
end
end
As the docs say, the type of the map will be:
val map :
('a Ppx_stage.code -> 'b Ppx_stage.code) ->
('a list -> 'b list) Ppx_stage.code = <fun>
So it takes a function that takes code and returns more code (higher level macros!), then it returns code that operates over a list of 'a types and returns a list of 'b types. It is type safe the whole way through.
As per the docs, let's now define a function to map with, since map is a macro (I.E. it takes code and returns code) then it needs to take code as well. Since this is only a PPX and not built into the language then we need it to be a macro too:
let plus1 x = [%code [%e x] + 1]
Now if this PPX were actually build into the language then by 'calling' a macro in position it would pass the AST in straight, but in the PPX world it's not that easy. ^.^
To use it you can just map plus1, which is of type and represented (inspected) as:
- : (int list -> int list) Ppx_stage.code =
let rec go = function | [] -> [] | x::xs -> (x + 1) :: (go xs) in go
The code is inlined and all!
Everything is fully typed, there is hygiene and polymorphic ast code and all as well, it really covers everything nicely, if it were integrated into the language proper.
One thing though, it needs any macro that is 'run' to be already compiled first, I.E. in another module, but that's a very common requirement in macro systems (look at rust's procedural macro's), but that also means the macros run at full speed, no interpretation (like Elixir's macro's, which are comparatively very slow when in-module).
Another project that is interesting to look at, an OCamlLabs (this is where new potential ocaml features are experimented with in forked compilers before being merged if they work out well) has an OCaml Macro's system, a blog post and code link (this isn't staged, it's more like C++'s constexpr system but more powerful, I guess this could be a phase macro system? Easier to implement (though not by much), doesn't have the restrictions of the macro 'functions' needing to be pre-compiled before use (or interpret them), but is strictly less powerful): https://oliviernicole.github.io/about_macros.html https://github.com/ocamllabs/ocaml-macros
This gives a good high-level overview of OCaml's Extension Points work: https://whitequark.org/blog/2014/04/16/a-guide-to-extension-points-in-ocaml/