Binding to Tagged template function

Is there a way to easily bind to js library that use tagged template function style ?
postgres in my case, but I also think of others such as styled-components, or graphql-tag

8 Likes

I have been lobbying for this for more than a year :grin:. Would make the current tagged template support in graphql-ppx less hacky.

2 Likes

So I guess no :disappointed:

The best way to do it at the moment that I am aware of is the following:

%%raw("require('gql')` your query `")

And if you need to pass something in:

%%raw("something => require('gql')` your query ${something} `")(something)
4 Likes

Our company definitely has use cases for tagged template functions, especially for postgres, it would be really cool to have something, especially given we have a very close syntax for string interpolation.
Maybe we could come up with some prototypes, what do you think @jfrolich?

Do you have an example of the postgres use case?

sure, we’re using PostGraphile that uses mainly two tagged template functions, a custom gql and pg-sql2.
You can find explanations and examples of their use here:
https://www.graphile.org/postgraphile/make-extend-schema-plugin/

2 Likes

@Maxim is it something you’d be open to see? Do you have any pointer on how to implement it?

TBH I don’t know. I guess it would require changes throughout multiple layers of the compiler. It’s more than parsing some syntax. (Could be totally wrong here)

How do you handle j and js tagged templates? It could be similar, right?

I’m still getting up to speed on ReScript internals so the following is based on my current and very incomplete understanding of how things work.

Caveat aside, modifying the parser shouldn’t be too difficult since ReScript already accepts the following:

let foo = gql`query {bar: ${bar}}`

It just ignores the gql part since it isn’t j.

The more difficult parts will be figuring out how to express tagged templates within the parse tree in such a way that they’ll be able to be type checked by the OCaml type checker. I think something where gql is defined as template_string -> string list -> a' or something like that.

This will also require that codegen to know gql in the generate JavaScript should be called as gql(template_string, ...list).

It’s indeed as Kevinbarabash is saying.
The current sql`select * from users;` syntax parses as the Pexp_constant parse tree variant. So you would need:

  1. A new concept in the ast to express js tagged template literals.
  2. Make sure it doesn’t conflict with existing syntax like j`string` .
  3. Have the type checker operate correctly on that new construct/encoding.
  4. Make sure error messages are good for the new construct/encoding.
  5. Modify codegen to emit the correct javascript code.
2 Likes

That would be a great solution for styled-ppx as well, but I’m entirely sure that neither graphqlppx neither styled-ppx wants to operate into template literals at run-time.

Maybe the comment made by @Hongbo about creating a new macro system can be influenced by this design?

1 Like

today

let foo = bar => baz`hello ${bar}!`

generates

function foo(bar) {
  return "hello " + bar + "!";
} 

How is this generated?

Basically in JS, tagged template

tag`blabla ${foo} bla`

calls:

tag(templateStrings, ...substitutions)

where substitutions.length == templateStrings.length - 1.

So in rescript you could make it call:

tag(templateStrings, substitutions)

where

let tag: (array<string>, array<'a>) => 'b

I totally get that it would require quite some changes to have correct error messages etc but it would erase the barrier with a whole set of JS tools. Bindings would be incredibly easy then since it would just need to write externals like:

@module("sql") @variadic external sql: (array<string>, array<sqlFragment>) => sqlQuery = "default"
4 Likes

this would allow easy interop with tools for GraphQL, styling, SQL and quite a lot of DSL.

2 Likes

Would the external declaration need to use @variadic for things to work correctly?

you’re right, I’ve edited the bindings.

1 Like

I made some progress on this today and was able to get the following code:

type sqlQuery
type sqlFragment = string
@module("sql") @variadic external sql: (array<string>, array<sqlFragment>) => sqlQuery = "default"

let table = "users"
let id = "5"

let query = sql`SELECT * FROM ${table} WHERE id = ${id}`
let jsStr = `SELECT * FROM ${table} WHERE id = ${id}`
let jStr = j`SELECT * FROM $table WHERE id = $id`

to compile to:

// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';

var Sql = require("sql").default;

var table = "users";

var id = "5";

var query = Sql([
      "SELECT * FROM ",
      " WHERE id = ",
      ""
    ], table, id);

var jsStr = "SELECT * FROM " + table + " WHERE id = " + id + "";

var jStr = "SELECT * FROM " + table + " WHERE id = " + id;

exports.table = table;
exports.id = id;
exports.query = query;
exports.jsStr = jsStr;
exports.jStr = jStr;
/* query Not a pure module */

Interestingly, I only had to make changes to src/res_core.ml in the syntax submodule.

There’s still some work to do (ensuring parse tree nodes have the proper locations, adding tests, etc.). I’ll post a link to the PR here once it’s ready.

7 Likes

I ran the tests in the syntax repo for the first time with these changes I ran into an issue with the printer tests. I have to update the printer to know how to convert the generate function calls back into tagged template strings. This should be possible by setting the res.template attribute on apply nodes generated from tagged template strings and then teaching the printer how to handle them.

1 Like

This is exciting @kevinbarabash , nice work!