Help writing a minimal macro

Consider the following code. I define a type once, then I repeat myself 3 times

  • create table
  • first part of insert values
  • second part of insert values

This then happens for EVERY struct/table I define. This is quite error prone, as I’m looking at tens of structs/tables.

In theory, I believe ppx can help me reduce this type of repetition. However, I’m new to ppx. Can someone help with a minimal sample macro? I’m mainly looking for a minimal “skeleton” I can then hack on.

module Row__Browser__Self_Event = struct
  type t = {
    self_id: Fixed_String_16.t;
    self_count: int;
    self_time: Time_Milli.t;

    tag_0: Fixed_String_16.t;
    tag_1: Fixed_String_16.t;
    tag_2: Fixed_String_16.t;

    data: string;
  };;

  let drop_tbl = {sql| DROP TABLE IF EXISTS main_logs.browser_self |sql};;

  let create_tbl = {sql|
  CREATE TABLE IF NOT EXISTS main_logs.browser_self
  (
      self_id          FixedString(16),
      self_count       UInt32,
      self_time_ymd    UInt32,
      self_time_hmsm   UInt32,

      tag_0            FixedString(16),
      tag_1            FixedString(16),
      tag_2            FixedString(16),

      data             String,
  ) ENGINE = MergeTree()
  ORDER BY ()
  |sql};;

  let insert_into = {sql|
  INSERT INTO main_logs.browser_self ( self_id, self_count, self_time_ymd, self_time_hmsm, tag_0, tag_1, tag_2, data) VALUES
  |sql};;


  let insert_values (v: t) (module Q: Single_Quote_Util) = String.concat " " [ 
    "(";
    Q.quote_string v.self_id.raw;
    Q.quote_int v.self_count;
    Q.quote_int v.self_time.year_month_day;
    Q.quote_int v.self_time.hour_minute_second_milli;

    Q.quote_string v.tag_0.raw;
    Q.quote_string v.tag_1.raw;
    Q.quote_string v.tag_2.raw;

    Q.quote_string v.data;
    ")"
  ];;


end

EDIT: Better minimal example:

module Row__Browser__Min = struct
  type t = {
    self_count: int;
    data: string;
  };;

  let drop_tbl = {sql| DROP TABLE IF EXISTS main_logs.browser_self |sql};;

  let create_tbl = {sql|
  CREATE TABLE IF NOT EXISTS main_logs.browser_self
  (
      self_count       UInt32,
      data             String,
  ) ENGINE = MergeTree()
  ORDER BY ()
  |sql};;

  let insert_into = {sql|
  INSERT INTO main_logs.browser_self ( self_count, data) VALUES
  |sql};;

  let insert_values (v: t) (module Q: Single_Quote_Util) = String.concat " " [ 
    "(";
    Q.quote_int v.self_count;
    Q.quote_string v.data;
    ")"
  ];;
end

I think this strips out all non-primitive types, yet still captures the “core” of the problem that there is one definition + 3 repetitions, and the 3 repetitions can be replaced via a macro.

Is there a readable impl of @@deriving fields from Records - Real World OCaml ? Understanding that macro should suffice for everything I need.

The ppxlib repo has two examples, one of which is a deriver, which derives accessor functions from a record type: ppxlib/examples/simple-deriver at main · ocaml-ppx/ppxlib · GitHub

If you are new to PPX, you might be interested in Preprocessors and PPXs · OCaml Tutorials (for some general introduction to PPXs) and Documentation · ppxlib 0.29.1 · OCaml Packages (for some material about writing PPXs).

2 Likes

@panglesd : Thanks for the instructive example. I typed up the code, and after fixing many typos I introduced, the code compiled.

For debugging purposes, how do I “printf” the output of the macro?

I.e. I want the output after macro expansion, but before it starts rewriting into whatever IR it uses.

I’m using dune for all my projects.

Thanks!

If you mean you want to see the code that the ppx generates (ie after expansion), you can use

$ ocamlc -stop-after parsing -dsource <file.pp.ml> > blah.generated.ml

That file.pp.ml will be in the _build dir.

Edit: this article from Tarides is a nice ppx intro if you’re looking for more reading materials.

1 Like

This is insane. I did not know you could specify SHELL SCRIPTS as “preprocessors” in dune. Truly fascinating.

1 Like