-
Notifications
You must be signed in to change notification settings - Fork 464
Tagged templates #6250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tagged templates #6250
Conversation
@cristianoc did we change something recently when it comes to the representation of |
in | ||
|
||
match prefix with | ||
| "js" | "j" | "json" -> genInterpolatedString () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain where the need to support json
is coming from.
Not sure if it's something you want to support, or if it's here just to fix some test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The need is not really to support json
but to avoid considering `json`something`
as a tagged template.
But now that js and j string interpolations have been simplified, we can likely refactor this function to something simpler.
My goal was to first make the original PR work, then start polishing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. The reason for asking is I would like to eventually remove that json thing entirely from externals too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK now I see again. I'm unsure why json
should e a special tagged template v.s. say my_json
. Is there a reason for that?
@tsnobip would you add a description at the top of this PR? It would help set the context on what this is doing. |
@tsnobip sorry before getting into the question about the implementations, some questions about the description above.
Not sure what that means. Could you give a self-contained example where |
@cristianoc I updated the example so it's more complete: @module("./tagged_template_lib.js") @variadic
external sql: (array<string>, array<string>) => string = "sql"
let table = "users"
let id = "5"
let query = sql`SELECT * FROM ${table} WHERE id = ${id}` should generate var Tagged_template_libJs = require("./tagged_template_lib.js");
var table = "users";
var id = "5";
var query = Tagged_template_libJs.sql`SELECT * FROM ${table} WHERE id = ${id}`; So now, if we consider a dummy implementation of the
but with the inlining, the compiler right now generates: var query = Tagged_template_libJs.sql`SELECT * FROM users WHERE id = 5`; which means the You can have a look at jscomp/test/tagged_template_test.res to see a whole (for now failing) test. |
There's a problem even before looking at the implementation strategy: let foo = (id) => {
Js.log(id)
let id = id ++ "_"
Js.log(id)
} gives: function foo(id) {
console.log(id);
var id$1 = id + "_";
console.log(id$1);
} so one cannot rely on |
In fact, I'm probably making too many assumptions. |
Another way of phrasing the question is: in the proposal, is there an assumption that tagged templates are preserved in the output, or can the compiler do anything as long at it implements the semantics of tagged templates? |
Indeed, we don't rely on the fact that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added quick review.
I think we can proceed with getting something that works. Then revisit a few points.
The one with json
I would like to understand better.
The rest is just implementation details.
jscomp/core/lam_pass_remove_alias.ml
Outdated
@@ -256,6 +257,18 @@ let simplify_alias (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = | |||
(Lam_beta_reduce.propagate_beta_reduce_with_map meta | |||
param_map params body ap_args)) | |||
| _ -> normal () | |||
in | |||
let result = (match result with | |||
| Lprim {primitive; args; loc} -> (match primitive with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting looks a little weird
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm for some reason autoformat is broken in my setup, I'll try to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no auto format. Long story.
For now manual format
jscomp/ml/translcore.ml
Outdated
@@ -985,14 +1005,14 @@ and transl_cases_try cases = | |||
in | |||
List.map transl_case_try cases | |||
|
|||
and transl_apply ?(inlined = Default_inline) ?(uncurried_partial_application=None) lam sargs loc = | |||
and transl_apply ?(inlined = Default_inline) ?(is_tagged_template=false) ?(uncurried_partial_application=None) lam sargs loc = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, invasive, but let's revisit later.
@@ -2251,6 +2264,64 @@ and parseBinaryExpr ?(context = OrdinaryExpr) ?a p prec = | |||
(* ) *) | |||
|
|||
and parseTemplateExpr ?(prefix = "js") p = | |||
let partPrefix = | |||
match prefix with | |||
| "js" | "j" | "json" -> Some prefix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This I'd also like to revisit.
Not sure why we need to consider json
. I'll need a reminder of why some issue with existing code and json
suddenly comes up when extending templates. Seems counter-intuitive that a pure extension brings new issues to existing code/examples/tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah something must be broken, I did this to make tests pass, but I agree it's ugly and there's no reason why we couldn't use json``
tagged templates, or js
and j
now that they don't make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be the relevant part:
let parse_processed = function
| None -> Some External_arg_spec.DNone
| Some "json" -> Some DNoQuotes
| Some "*j" -> Some DStarJ
basically, the delimiter determines how the string is emitted: whether in quotes, etc.
And this is going to interfere.
in | ||
|
||
match prefix with | ||
| "js" | "j" | "json" -> genInterpolatedString () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK now I see again. I'm unsure why json
should e a special tagged template v.s. say my_json
. Is there a reason for that?
I agree with your plan! |
My suggestion is to:
The result should be type safe by design, and zero cost in the simple cases. |
Then perhaps unify with the existing template mechanism, where the operator is string concatenation, the state is string, the conversion function for strings is the identity, and for values it is also the identity. |
Apologies for the confusion earlier. In the revised code, we're using a simplified version of how tagged templates work in JavaScript, and applying it to ReScript. Here's a summary: The revised code breaks down the tagged template into its constituent parts and manually processes each one.
Simplified code by inlining the construction functions would be as follows: let table = "users"
let id = "5"
let query = ("SELECT * FROM " ++ "'" ++ table ++ "'") ++
(" WHERE id = " ++ "'" ++ id ++ "'") In terms of correctness, this implementation correctly mimics the behavior of tagged templates in JavaScript. It handles the string and value parts separately, and constructs the final string in the correct order. As for encapsulation, the However, this implementation is more verbose and manual than using tagged templates in JavaScript, which handle the splitting and combining of string and value parts automatically. As of my knowledge cutoff in September 2021, ReScript did not support tagged templates, but the pull request you mentioned earlier would add this feature. |
// Tag handler function definitions
let stringHandler = str => str
let valueHandler = value => "'" ++ value ++ "'"
let combiner = (state1, state2) => state1 ++ state2
// Definition of sql as a triple of handler functions
let sql = (stringHandler, valueHandler, combiner) |
@cristianoc all those proposals are interesting but then we lose 0 cost bindings to JS tagged templates which is the biggest perk of this feature, shouldn't we focus on this first? What do you think @zth? |
I am OK with 0 cost bindings to JS tagged unions. |
yeah we have to make a decision here, we could ask on the forum what would be the preferred use case. |
Definitely. The community should also indicate that this feature is needed. |
Ok, so let's try and sum this up and the various paths we can take. This was not made clear enough from our side (me + @tsnobip who's been talking about this) from the start, but the primary reason for building native support for tagged template literal functionality is to integrate with the broader JS ecosystem. Rolling our own solution to the same problem that tagged template literals "solve" is also quite interesting, but it wouldn't be solving the same problem, which is integration with the broader ecosystem. Integrating with the broader ecosystem means two things (to me):
So, to sum up my stance, if this feature is to be useful, it should (in falling priority):
As for whether this is something the community wants, here's a few issues and forum posts related to tagged template literals: Here's a few examples of popular JS libs relying on tagged template literals for ergonomic (subjective of course) APIs:
Outside of this I think it could open up some pretty interesting possibilities for building libs leveraging tagged template literals in pure ReScript as well. Now, with all of that, my personal view is that this feature would be valuable and would plug a hole that would allow certain use cases much more naturally. But, it's probably no where near as important as for example async/await was, in terms of what the community needs/wants. So, I think it comes down to how complex this functionality will get. If it doesn't complicate things too much I think it's worth it, and I think it would serve a good purpose. But if it needs moderate to highly complex changes to the compiler then it's probably not. These questions come to mind from my end after following this PR and discussion:
@cristianoc @tsnobip what are your thoughts on this? |
After offline discussions we've decided to pause working on this for now. We will likely revisit at some point in the future. |
Any context why? Is it because it's very hard to prevent inlining? |
Sorry for dropping the ball on this, my fault. In order to move this forward, we need to more clearly state:
Again, on me for not being clearer from the start. |
ok, so for my use case I 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, 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. |
might be better to use the forum to list those use cases, I've started doing it in the RFC. |
c1f4f12
to
56b8642
Compare
1c6ecd3
to
c1d2a03
Compare
589206d
to
8bd1dfd
Compare
@cristianoc @zth I simplified the implementation a lot, we now have to add a Let me know what you think |
Looks great! |
@tsnobip ready to merge? |
8bd1dfd
to
dc9cbd0
Compare
Why allow both though? Pick the most idiomatic sounding one unless there's strong reasons to have both. |
haha mostly to avoid bike-shedding, most "historical" annotations use snake_case ( |
Yes, definately. 😀 |
dc9cbd0
to
949af10
Compare
OK we're all good then, I've enabled auto-merge (squash and merge) |
Perfect! Great work! |
The goal of this PR is to support tagged template literals in Rescript, primarily to benefit with externals from JS tools like
sql`some sql query`
orgql`some graphql query`
.should generate
The tag function should have the following signature in rescript:
where:
These two expressions should be equivalent:
This PR is originally based on kevinbarabash#2.
I'm trying to make it compile, remaining to fix:
@as(json`{}`)
. in UncurriedExternals.res@local
not working in lam_pass_remove_alias.ml