[RFC] Support for tagged template literals

Resurfacing from this thread. Binding to Tagged template function - #37 by zth

Tagged Template RFC

I’ve compiled this thread’s discussions into a single post here for renewed discussion. Github issue to come.

Popular JS libraries implement tagged templates (MDN) to provide some really ergonomic APIs. Rescript should make their use feel easy, whether directly supporting the syntax or compiling it into the desugared version of tagFun(expressionsArr, ...args)

Common use cases for both frontend and backend

  1. Common SQL libraries, like prisma.$rawQuery use the sql-template-tag library to form prepared statements.

    sql`select * from table where id = ${val1}` // val1 is escaped
    

    Great improvement over the typical

    sql("select * from table where id = ?", [val1]) 
    
  2. Frontend css-in-js libraries heavily use tag template literals, i.e. styled-components.

    const Button = styled.button`
      color: grey;
    `;
    
  3. graphql-tag uses tagged templates

    gql`
       {
         user(id: 5) {
         firstName
         lastName
       }
      }
     `
    

Current solutions

  1. Use raw to directly directly inline the tagged template. referenced post.

    %%raw("require('gql')` your query `")
    
  2. Create bindings that map to the desugared form (terrible, and I’ve yet to do this myself). referenced post

    @val @variadic
    external gql:  array<string>  => array<'a> => int = "gql"
    
    let result = max([5, -2, 6, 1])
    

Prior art - what’s been done so far

@kevinbarabash 's done alot of exploration and have submitted PRs 1 and 2 to have Rescript compile Rescript tagged templates into the js desugared form (see post).

The main blocker to this possible solution is to have the rescript syntax understand when a tagged template function is called. See @Maxim 's response.

Where to go from here? Can we add native support into rescript?

The team and community should determine the best solution and outline a proper implementation plan with dev costs.

10 Likes

My desire for this feature is relatively moderate as it’d let me use our prisma and sql in a team-appetizing, but not enough for me to dive into the compiler just yet. :slight_smile:

There’s alot of work done already, but no consensus on what’s actually acceptable.

2 Likes

We can all probably agree that this is a desired feature. It will however most likely need substantial amounts of work still, even with the exploration done. Let’s see if someone can pick this up at some point.

1 Like

I’m interested in doing this myself too. But am hacking on another ocaml project atm.

I guess a good starting point here for anyone is to introduce the tag template syntax to the syntax repo.

3 Likes

The syntax repo is now merged with the compiler, so working on this should be a lot easier.

2 Likes

Ty. I’ll likely be able to take this up late March. Unless someone else decides to do it

2 Likes

Let us know if you’re still interested in looking into this.

I’m unable to pick this work up :face_with_peeking_eye:

Some excitement - @tsnobip has revived the great base work done for this feature by @kevinbarabash, and has gotten quite far in finishing it: GitHub - rescript-lang/rescript-compiler at tagged_templates

7 Likes

ok, so let’s enumerate here the use cases we have for tagged templates in rescript so we can have a better understanding of this feature and its priority level.

I’ll start: use rescript to write GraphQL servers backed by a Postgres DB using postgraphile.

This makes me query the DB quite a lot, and hence, write SQL queries. You have basically two ways to do it,
you either use parameters like this:

query(
  pgClient,
  ~statement="select * from app_private.create_media($1, $2, $3, $4, $5)",
  ~params=[uuid(productId), string(path), string(filename), float(size), string(sha256)],
)

but it’s very brittle when you start having a significant number of arguments and start modifying them, so you could instead use tagged templates like this:

query(
  pgClient,
  sql`select * from app_private.create_media(${uuid(productId)}, ${string(path)}, ${string(filename)}, ${float(size)}, ${string(sha256)})`
)

This solution is way more robust but is not doable right now in rescript.

I also write GraphQL schema language using a gql tagged template function, but using tagged templates manually is so complex and error prone I just make it a single string and don’t use parameters, copy pasting blocks instead of using parameters, which is of course not ideal.

6 Likes

Another use-case for tagged template literals: the bun shell.

2 Likes

Nice!! Well the good news is that the PR now works, so I guess it’s a matter of polishing and shipping it if we find the feature interesting!

2 Likes

This would be awesome to have!

I recently did a small project using FaunaDB and I had to write my queries in JS files since they used a syntax like this.

fql`myData.all()`
3 Likes

I’m testing this out with Fauna and I am hitting a snag.

Here’s some code:

// binding
@module("fauna")
external fql: (array<string>, array<string>) => query = "fql"

// usage
let postQuery = fql`blog.all()`

// output
var postQuery = Fauna.fql(["blog.all()"], []);

Fauna throws an error Error: invalid query constructed.

I did some poking and the way to resolve this is to remove the second array:

var postQuery = Fauna.fql(["blog.all()"]);

I also tried querying by an id to see what happened if I pass a parameter.

let id = "388452496460218432"
let postQuery = fql`blog.byId(${id})`
let posts = await client.query(postQuery)

// output
var postQuery = Fauna.fql([
      "blog.byId(",
      ")"
    ], ["388452496460218432"]);
var posts = await client.query(postQuery);

This has a different fauna error: QueryCheckError: The query failed 1 validation check

You need @taggedTemplate on the external binding.

1 Like

Yup, that fixed it. Thanks!

2 Likes