ReasonML (also known as Reason) has gotten very popular these days. It's a new syntax for OCaml that looks a lot like JavaScript, allowing you to develop with a rock-solid type system while still having strong interop with JavaScript and its strong ecosystem.
That was a mouthful, I know. For a proper introduction (if you aren't familiar), I'd highly suggest reading through the official Reason docs!
Now let's get into the point of this post - Reason PPXs.
What is a PPX and Why Do I Care?
A Pre-Processor eXtension (PPX) is a preprocessor that is applied to your code before it gets passed to the compiler. It allows for arbitrary manipulation of your code's Abstract Syntax Tree, rather than the code itself.
For example, if you have ever written GraphQL queries in your Reason code, chances are you've used [%graphql {|QUERY|}]
to validate your queries - this is a PPX! In the case of graphql_ppx
, your query will be validated and is guaranteed to be type-safe. The query is then transformed into a normal string before your code is compiled.
Another example is tailwind-ppx
, a PPX I wrote to validate your Tailwind CSS classes at compile-time. Using this PPX, you can write your class names like <Component className=[%tw "flex flex-row"] />
and get validation for zero extra cost (in case there's a typo in a class name or you included a class name twice, for example).
PPXs are very powerful, but I found it rather difficult to get a barebones one up and running. Though I tried hello-ppx-esy
and eventually found some success, it seemed that the process should be friendly to both Reason newcomers and PPX newcomers, rather than just the latter. Namely, I felt that (1) Reason newcomers should not have to use esy, (2) it should be more natural to run tests, and (3) publishing your PPX should be straightforward.
hello-ppx-bucklescript
hello-ppx-bucklescript
is a small Github project I wrote a while back that provides the boilerplate for writing a Reason PPX. It takes care of configuring the BuckleScript compiler, hooking up tests, and generating your PPX executable. hello-ppx-bucklescript
was inspired by hello-ppx-esy
, but differs in that you do not need esy
, running tests is simple with yarn test
, and it includes a publish folder for making your PPX immediately available on the NPM registry.
Project structure
publish/
- basic setup for publishing your PPX to the NPM registry.
src/
- contains the source code for the PPX.
tests/
- a single test file that can be run with yarn test
PPX source code
The ppx included in the project essentially turns the [%hello]
expression into the integer literal 42
at compile-time. The code to do this operation is this:
open Ast_mapper;
open Parsetree;
let expr = (mapper, e) =>
switch (e.pexp_desc) {
/* If the expression is [%hello] */
| Pexp_extension(({txt: "hello", loc, _}, _payload)) =>
/* Then replace by 42 */
Ast_helper.Exp.constant(Asttypes.Const_int(42))
| _ => default_mapper.expr(mapper, e)
};
let mapper = _ => {...default_mapper, expr};
let () = run_main(mapper);
Let's walk through each part.
open Ast_mapper;
open Parsetree;
Use the open directive to make the Ast_mapper and Parsetree modules directly accessible by the Hello module. See here for more information on modules.
let expr = (mapper, e) =>
switch (e.pexp_desc) {
/* If the expression is [%hello] */
| Pexp_extension(({txt: "hello"}, _)) =>
/* Then replace by 42 */
Ast_helper.Exp.constant(Asttypes.Const_int(42))
| _ => default_mapper.expr(mapper, e)
};
expr is a function that takes in a mapper and an expression and returns a new expression. For the [%hello]
PPX, expr
is the function that we care about most.
switch (e.pexp_desc) {
...
}
Here we're going to switch on the value of the pexp_desc
property of the input expression. pexp_desc
is of type expression_desc
, and thus can have many different constructors (see its type here) - however, all we really care about is if it's a PPX extension (Pexp_extension
) and the extension name is hello
!
| Pexp_extension(({txt: "hello", _}, _)) =>
...
If we fall into this case our expression matches the Pexp_extension
constructor, and as a result, we need to modify the AST! We can ignore the other properties of a Pexp_extension
, since all we care about is that the txt
(the extension name) is hello
and not something like graphql
or tw
. We'll replace the AST with the integer literal 42
, and then we're done!
| Pexp_extension(({txt: "hello", loc, _}, _payload)) =>
/* Then replace by 42 */
Ast_helper.Exp.constant(Asttypes.Const_int(42))
That's it! The rest of the code is just PPX boilerplate that doesn't need much conceptual understanding. Feel free to ask questions below if you're confused, though!
Sources
I could not have written this article, nor created hello-ppx-bucklescript
or tailwind-ppx
, without the following [%incredible]
sources:
- https://blog.hackages.io/reasonml-ppx-8ecd663d5640
- https://github.com/jchavarri/hello-ppx-esy/
- https://tarides.com/blog/2019-05-09-an-introduction-to-ocaml-ppx-ecosystem
Thank you for reading! And please check out tailwind-ppx
if you're thinking about building a new ReasonReact + TailwindCSS project!
Top comments (0)