tc39 / proposal-string-cooked

ECMAScript proposal for String.cooked built-in template tag
MIT License
52 stars 5 forks source link

Name bikeshed #5

Open jamiebuilds opened 3 years ago

jamiebuilds commented 3 years ago

Splitting this out from #1 cause this tends to be a big topic.

options:

String.cooked

The primary appeal of this is to contrast it against String.raw. A potential downside to that name is that it doesn't really match the intended use case of this function: To make it easier to implement "value pre-processing" tagged template functions.

String.interpolate

This function aligns a bit more with the intended usage of this function, and is overall a bit more literal about what it does internally.

String.identity

This name has been brought up before, it refers to the fact that these two are equivalent:

let a = String.identity`tag`
let b = `tag`

The downside is that using this function as the direct tag of a tagged template is not really its intended use case, and calling it within another tag makes it a somewhat weird name.

Other names

Feel free to suggest other names, but if you do, please explain what that name accomplishes and what it loses out on from the other names already suggested.

bakkot commented 3 years ago

+1 for String.cooked.

On reflection, I also like String.interpolate. Sorry to be unhelpful!

tabatkins commented 3 years ago

Pro for String.identity is that if you're doing something that's meant to take a tag function (and which will then call the function on a template string at some point), identity immediately suggests its role as a default/no-op value; none of the other names do. Con is that it suggests you'll get the exact string you put in if there is no interpolation, which isn't true - the escapes are resolved.

I quite like cooked. It's a little silly, but it has several good points for it:

Not a fan of interpolate - all tag functions interpolate (assuming you're referring to the ${} segments). The important notion to capture here is what it does to the literal segments, vs raw.

michaelficarra commented 3 years ago

Since you're looking to match the behaviour of an untagged template, why not something like String.asIfUntagged or just String.untagged? It can be thought of as the "implicit tag" for a template when one isn't provided.

In a recent refactoring of TV and TRV, we chose to refer to the cooked properties as the "indexed components" of the template (since the only reference to the term "cooked" currently is in alias names), in contrast to the "raw components". Though I think String.indexed doesn't really do the job here.

I am fine with either of String.interpolate or String.identity. I would avoid String.cooked since, as mentioned above, it's not normatively referenced or defined in the spec, and the usage of the term seems to be TC39-internal.

bakkot commented 3 years ago

I would avoid String.cooked since, as mentioned above, it's not normatively referenced or defined in the spec, and the usage of the term seems to be TC39-internal.

raw is in the language, and cooked is an obvious analogue to raw which suggests precisely the behavior it has.

tabatkins commented 3 years ago

I had no idea we even used the term "cooked" internally in the spec. It just sounds like a nice analogue, and lightly conveys the precise meaning intended here - the string pieces have been allowed to "mature" and change to their final forms.

jamiebuilds commented 3 years ago

One downside for names like String.zip, String.interleave, possibly String.interpolate, and arguably String.identity is that their method names on their own are common names of much more generic functions in other languages (i.e. IterTools.interleave) / JS libraries (i.e. lodash.identity), and are names we may consider in the future for an IterTools JS proposal, or something else.

jamiebuilds commented 3 years ago

Also to mention, I've seen a number of compilers in different languages use the term "cooked" to distinguish it from "raw". In Rome we use it like this:

jsTemplateElement.create({
  cooked: "",
  raw: "",
})
rbuckton commented 3 years ago

"cooked" its not exactly common nomenclature in JS. I'd suggest String.interpolate or String.interp. Having String.interpolate doesn't preclude other kinds of interpolate functions/methods from other sources with a different meaning. I'm not sure how often I see interpolate as a standard function (I've generally seen either lerp for linear interpolation or tween for animation frame interpolation/easing). Also, the fact it is namespaced on String makes it fairly obvious its intended use.

senocular commented 3 years ago

FWIW, MDN refers to "cooked" strings within their template literal documentation. Excerpt:

However, illegal escape sequences must still be represented in the “cooked” representation. They will show up as undefined element in the “cooked” array:

function latex(str) {
  return { "cooked": str[0], "raw": str.raw[0] }
}

latex`\unicode`

// { cooked: undefined, raw: "\\unicode" }
GreLI commented 3 years ago

Could it be String.parsed? Just like JS engine parses template literals. For me 'cooked' feels like a synonym for 'parsed'.

bathos commented 3 years ago

@GreLI that's an interesting angle.

It might be a bit of a squatting hazard given JS does already have Number.parseFloat - which parses a regular number token (afaik; not 100% sure it's free of quirks) and analogous companions (Boolean.parse, String.parse) don't strike me as too far fetched to consider leaving space for. Tags can only address raw/cooked string values rather than string tokens - the former are products of parsing, the latter is input to parsing (w/ the quotation marks, etc).

OTOH, "parsed" is not "parse", and I can see how that could make the difference.

Speaking of that distinction: I think parse v parsed is a great example of why nouny/adjectival naming for tags may be preferable over verb names in general - it seems particularly clear when contrasting these that the first would be "off". Raw is adjectival, too, and while a precedent of one may not count for much, (anecdotally) it seems like the convention of tag names communicating something the string is rather than what's being done with it is pretty widespread (e.g. "html", not "createTemplate"). So (for example) I'd suggest "interpolated" is better than the previously discussed "interpolate".

jamiebuilds commented 3 years ago

At first glance, the past-tense parsed doesn't feel much like JavaScript's existing stdlib, I'm not sure if anything is named that way, but a quick look at MDN for global object methods the only one I was able to find was Promise.allSettled

In a more general sense, "parsing" to me implies syntactic analysis of a string or the transformation between data types, neither of which feel terribly relevant here.

Looking around for alternatives I found that Golang's grammar uses "raw" vs "interpreted". I have similar reservations as I do parse, but I think methods named interpret are few and far in between so maybe that's fine.

bakkot commented 3 years ago

I wanted to use this function in a template tag the other day in the way suggested in the readme:

function myTag(strings, ...exprs) {
  let transformedStrings = strings.map(someTransformation);

  return String.cooked(transformedStrings, ...exprs);
}

This made me like cooked less. By the time I've prepared the data (in this example), it's no longer a template-strings array: it's just an arbitrary array of strings.

That is, I've already processed the strings at the point at which I want to call this function. So any name which suggests that it is processing the strings themselves, rather than just mashing them together, is a bad name. (For example, parsed has exactly the same problem as cooked.)

So my vote is now for interpolate or similar.

(This usage also makes it clear, I think, that identity is a bad name. identity only makes sense when it's used as a template tag, not when used for building other template tags.)

bakkot commented 3 years ago

I could also see a case for splitting out the two use cases: have a String.interpolate(array1, array2), for use in building template tags, and a String.identity(array1, ...exprs) for use as a default template tag.

benatkin commented 2 years ago

How about pasteurized?

Jbonez87 commented 1 year ago

I like String.interpolate the most.

benatkin commented 1 year ago

I think cooked is good, better not to worry about it and be happy with a carefully considered name even if there are other options. It's a feature, not a bug, that it uses a different term than golang even though I like golang and think their name is also fine. Being a contrast against raw, it is a solid choice.

ptomato commented 1 year ago

I think I might have mentioned this in a TC39 plenary meeting at one point, but I couldn't find it in the notes with a quick look. In general I think using puns to name things can be confusing for developers (numbering likely in the millions or tens of millions worldwide) who aren't fluent in English.

benatkin commented 1 year ago

I suppose with general purpose translation tools "interpolated" would do better but with docs in an editor or a search it would be quick to find the meaning.

As for raw vs cooked, there are a lot more definitions for raw than uncooked. So it's not necessarily raw as in food, though I'm sure a lot think of that. I have thought of it as raw as in material, like raw wood. The word to describe prepared wood isn't "cooked". Even for food it breaks down w/ pasteurized as I mentioned.

I might have changed my choice to interpolate. 🤔

bathos commented 1 year ago

Tag names should be descriptive/declarative, not verbs. I’m not married to a particular name, but I think that factor is important. Old meeting notes suggest this reasoning was used for raw itself, and in the years since, the ecosystem has generally stayed true to this convention too — tags get named things like html, not parseHTML, etc.

A name like interpolated would meet that criteria, but the “imperative” interpolate would not. In any case, I believe the latter should be reserved for a non-tag utility method, which has its own reasons to exist.

benatkin commented 1 year ago

It's only technically a tag name, though. It doesn't need to be a particularly good example of what to call a userspace tag name IMO.

I do agree that they aren't imperative verbs like a function. I think the past participle would work. So I suppose String.interpolated would objectively be better.

Maybe that can be considered descriptive and declarative even though it's just changing the tense.

bergus commented 3 months ago

I have thought of it as raw as in material, like raw wood.

Yes, I'm pretty sure that's the intended original meaning. It returns the raw source code string. I would argue that the opposite term is processed, not cooked. I also like parsed or interpolate(d), possibly interpolateTemplate if required for disambiguation, but please avoid the food analogy.

devingfx commented 1 month ago

Hi ! I have an utility String.merge in my utility repo for years doing this... (maybe with some differences as this spec, like defaulting undefined values to "" to don't throw)

Definition here: https://github.com/devingfx/vanill.es/blob/dev/String/merge.js Referenced as String.merge here: https://github.com/devingfx/vanill.es/blob/4910bdd4fb4c8ecf7cf0f58779ec335c4fddbde2/String/index_.js#L8

Why merge ? Because this is more about "concatenating", "sticking", "zipping" the strings parts and the placeholder values, that an inverse of the "raw" notion in my head...

It is used to kinda "get what would be the result of untagged string" but in an actual string tag function... Example: https://github.com/devingfx/vanill.es/blob/dev/DOM/CSS.js

ljharb commented 1 month ago

merge sounds very similar to me as concat, which is basically just +.

devingfx commented 1 month ago

merge sounds very similar to me as concat, which is basically just +.

Yes exaclty !
Which is basically what this method is doing:

String.merge`Hello ${who} !` === 'Hello ' + who + ' !'
zeel01 commented 1 month ago

Interesting proposals, I do love the symmetry of String.cooked with String.raw, but I also agree that taken independently "cooked" is a bit unclear.

Over all, I sort of think the problem is that most of these are verbs (or adjectival uses of things that are also verbs). Only identity stands out as not implying that the tag does something or returns the result of any process. But it also goes a bit too far in that aspect, in that one might think it causes the template to actually do less than a normal template.

As an alternative, might I present:

String.tag

You might being thinking "that doesn't even mean anything!" and you would be right! That's the point. Because String.tag doesn't say what it does it says what it is which is exactly what it's supposed to do. It is the "identity tag" in that it is the identity of a tag, it is the simplest possible tag that doesn't do anything but exist. It implies no function, nor does it imply a lack of function.

Furthermore, if you used it as many people likely will:

const html = String.tag;
const css = String.tag;
const sql = String.tag;

It becomes extremely clear that you are just making these identifiers string tags. This also has the advantage of being short, and even happens to be the same number of characters as String.raw so if someone wanted to replace all incorrect uses of String.raw with the correct new tag, this won't move a single character.

It means nothing, it does nothing. It's just there for other tags to build upon.

That being said, I think of the other proposed terms "cooked" is the one I like best purely because "raw" already exists. I also think that its lack of contextual meaning at least means it probably won't be misunderstood, the advantage of a potentially non-obvious name is that few people will assume they know what it means and will look it up. This is as opposed to "interpolate", "identity", "parsed", and similar which someone could easily misinterpret without even stopping to think "do I actually know what that means?"

gibson042 commented 1 month ago

String.tag isn't bad. And running a bit further in that direction, I'd propose String.fromTemplate:

function safePath(strings, ...subs) {
  return String.fromTemplate(strings, ...subs.map(sub => encodeURIComponent(sub)));
}

let id = 'spottie?';

safePath`/cats/${ id }`;

// → "/cats/spottie%3F"

It follows the existing "$Type.from$Thing" pattern of Object.fromEntries/Uint8Array.from{Base64,Hex}/Array.fromAsync, providing a clear suggestion of how to read it and what to expect it does, and makes sense independent of any comparison to String.raw (e.g., consider let buf = String.fromTemplate(["This ", " is *cooked*!"], "string"); as basically the JS transliteration of C sprintf(buf, "This %s is *cooked*!", "string");).

zeel01 commented 1 month ago

I like String.fromTemplate, it makes a lot of sense when called directly which is likely how this method will be used most. We have String.fromCharCode() and String.fromCodePoint() already. What about just String.from?

I do think it's slightly more redundant though, just as String.interpolate implies doing something that the template tag already does, String.fromTemplate also sort of implies that the template produces something that isn't a string and needs to be converted to one. But I like it better regardless.

devingfx commented 4 weeks ago

I may also purpose:

devingfx commented 4 weeks ago

What about just String.from?

Usually a static .from method can take anything and convert it to related class. I would imply that the input is not especially a template literal arrays but anything, so it duplicates .toString() somehow...

String.fromTemplate also sort of implies that the template produces something that isn't a string.

I do not agree with this point: get String from Template is obvious about what the result will be...

devingfx commented 4 weeks ago

String.glued ?

zeel01 commented 4 weeks ago

We generally don't see abbreviations in the language, and I think that's a good thing. While fromTemplate might be long, it's clear. While fromTpl is confusing, and fromLit sounds like it's from the Lit framework.

My point about "not doing anything" is that template literals already do this, they already interpolate/merge/zip/concatenate inherently. If I write this:

const content = "Inner HTML!";
element.innerHTML = `<div>${content}</div>`;

That already does all of the functionality, that will merge the template together and product a string that will be assigned to innerHTML.

Using this String.cooked method doesn't change that, its entire purpose is to not actually modify the behavior of the template literal. Rather, it has two alternative purposes:

My example expanded:

const html = String.tag;
const content = "Inner HTML!";
element.innerHTML = html`<div>${content}</div>`;

Note that the html template tag doesn't do anything that the template doesn't already do. But my IDE can detect that tag, and switch the syntax highlighting mode to HTML within the template. This is a highly desirable feature.

The other use is as the basis for tags that do actually do something, but don't modify the templating. For instance, I might implement an actual html tag that returns a new DocumentFragment instead of a string.

String.fromTemplate also sort of implies that the template produces something that isn't a string.

I do not agree with this point: get String from Template is obvious about what the result will be...

The result is obvious, the implication is not. A template already creates a string, the result of my first example with no tag is a string. Using a template doesn't produce some specialized object, it produces a string, as such:

String.fromTemplate`<div>${content}</div>`;

Is arguably redundant (though not as bad as many of the others). This statement doesn't create a string from a template, it actually just gives you the result of the template - hence why String.identity is a suggestion, it is essentially a no-op.

The muddy water comes from using the method in order to build a template tag such as @gibson042 wrote:

function safePath(strings, ...subs) {
 return String.fromTemplate(strings, ...subs.map(sub => encodeURIComponent(sub)));
}

That tag does actually modify the way that the template is interpolated. This is why the naming is so difficult, because in this use case there is a good argument for a name like String.zip while in my previous example zip wouldn't make sense at all.

Still, I think String.fromTemplate is among the better options because it at least has some precedent in terms of naming convention, and the slightly wrong implication I noted earlier isn't nearly as big as the implications of String.interpolate. Furthermore, fromTemplate is pretty equally logical both as tag and when used to implement a tag.

Still, I think String.tag takes a step even further, by representing "The tag function" and nothing more.

As for collage, concat, alternate, add, glued, etc. These all have the same issue as with interpolate or parsed: They only make sense in the context of implementing another tag function, they only make sense when called with the array and strings. If you do:

String.glued`<div>${content}</div>`;

...it doesn't really make much sense, and at best implies that the template requires a tag in order to function (which it does not). And if you do const html = String.glued; it's likewise unclear what you're trying to do.

Edit: Mixing markdown and templates is... not great.

devingfx commented 4 weeks ago

But my IDE can detect that tag, and switch the syntax highlighting mode to HTML within the template. This is a highly desirable feature.

+1 for this argument, I also use this VSCode extension, but for your information it work also with a comment:

/*html*/`<div>${content}</div>`

;)


I almost agree with all the rest of your last arguments, except I really don't see the point of using this "noop" function as a tag directly, it is the same as not using a tag... I feel this usage would be rather rare ! Even with your exemple of noop html tag, you are already renaming it so the original name does matter most...

devingfx commented 4 weeks ago

String.asIs ?

bakkot commented 4 weeks ago

If this is something that we expect people to use as both a function and a tag, then the name needs to suggest that the signature is what you'd get when using it as a tag, because that signature is not what you'd normally expect for something you call as a function. Which means fromTemplate or defaultTag or possibly cooked (by analogy to raw) all work, but nothing along the lines of glued or asIs or concat.

devingfx commented 4 weeks ago

What is the point of using it as tag ? It only makes your source code bigger, and adds useless virtual machine's rountrips decreasing performance ! Everytime you would use it as a tag, you better have don't use tag for performance...

BTW the tag's signature is indeed needed to be able to pass arguments around easily :

const myTag = ( ...args )=> {
    const sanitized = utils.sanitizeTemplateLiteral( ...args )
    let result = String.cooked( ...sanitized )
    result = result.replace( /foo/g, 'bar' )
    ...
}
bakkot commented 4 weeks ago

What is the point of using it as tag?

For example:

function renderIntro(name, needsSanitation) {
  let tag = needsSanitation ? sanitize : String.fromTemplate;
  return tag`Hello ${name}`!
}

BTW the tag's signature is indeed needed to be able to pass arguments around easily :

No. If we expect this not to be used as a tag, then the obvious signature is to take two lists. You can just write template, ...exprs everywhere you are writing ...args in your example. Having to do this instead of ...args is not worth the less-obvious signature. The only reason to use this signature is so it can be called as a tag.

devingfx commented 4 weeks ago
function renderIntro(name, needsSanitation) {
  return needsSanitation
    ? sanitize`Hello ${name}!` 
    : `Hello ${name}!`
}
function renderIntro(name, needsSanitation) {
  let name = needsSanitation ? sanitize(name) : name
  return `Hello ${name}`!
}
bakkot commented 4 weeks ago

Please imagine a more complex template expression, such that you wouldn't want to repeat the whole thing or sanitize each individual expression, as was obviously my intent.

zeel01 commented 4 weeks ago

What is the point of using it as tag ? It only makes your source code bigger, and adds useless virtual machine's rountrips decreasing performance !

You would be unlikely to author code with it used directly as a tag rather than "renaming" it by assigning it to some other identifier. However picking a name under the assumption that "nobody will ever use it like that" isn't great, and at the very least there will be examples written on sites like MDN that absolutely will show it used in that way in order to demonstrate what it does/doesn't do.

And as @bakkot pointed out, whether or not anyone uses it directly as a tag, the signature of the function has to match the signature of a tag function in order for this sort of "renaming" to work in the first place. As such, the function will be a valid tag function even if using it as one directly is seemingly pointless. Though I'm not really convinced that it's truly pointless, I just haven't come up with a good reason to use it without a rename yet.

but for your information it work also with a comment

Yeah, unfortunately the comment way doesn't always work correctly and it's needlessly verbose. Plus it's a little weird to use both methods. For instance in a project that actually uses Lit, the html tag is a real thing that does something, but you might also use SQL someplace and for consistency want a sql tag rather than /* sql */ comment.

Another neat thing to do with these, is have a debug-only behavior. For instance:

function debugTemplate(strings, ...values) {
   const string = String.cooked(strings, ...values);
   console.log(string);
   return string;
}

const sql = DEBUG ? debugTemplate : String.cooked;

SQL in particular I commonly need this because SQL errors are almost always totally unhelpful, and being able to copy-paste the fully interpolated query into another tool to check it over is invaluable.

devingfx commented 4 weeks ago

@bakkot Okay for the non duplication of template, it's why I wrote a 2nd exemple... The strings parts of your template won't ever have to be sanitised, it's your source code! Only values part may have to be sanitized...

Still you can process the strings part if you want or need but the case fall back to the non tag usage because you will need the strings array therefore your function to be a tag...

devingfx commented 4 weeks ago

@zeel01 I agree on the tag's signature as said before...

For your SQL exemple:

const query = ` blah ${table} blah ...`
DEBUG && console.log( query )

Or if a sql tag is needed to do actual jobs:

const sql = ( ...args )=> {
  const query = String.{choose a name in this thread} ( ...args )
  DEBUG && console.log( query )
   return  MySQL.exec( query )
}

const rows = sql`SELECT ...`
zeel01 commented 4 weeks ago

Examples are intentionally trivial, refactoring a trivial example doesn't really add anything to the conversation.

Imagine I have a module with 30 SQL queries in it. I don't want to assign every query to a temp variable, then log that variable, then run the query. Instead, I want to simply pass the query into the SQL library I'm using.

The tag function lets me sneak a function call in between creating the string and sending it to the query - one that doesn't add more than 3 characters for each use, and as a bonus makes the IDE switch syntax modes. As such I can write all the queries in a normal natural way, and enabling debugging is done elsewhere with only a couple of lines rather than debugging logic being mixed into every single query.

zeel01 commented 4 weeks ago

In a way, tag functions are very similar to Decorators, they essentially decorate a string rather than a class or function. They allow you to encapsulate some logic behind a simple declarative syntax, and the particulars can be worked out elsewhere.

devingfx commented 4 weeks ago

I agree that this is a bit off topic... But what I wanted to point out is: Does this spec's function really need to used as a tag direcly? And this will drive the name choice because the name will also "force" the usage...

zeel01 commented 4 weeks ago

The point is that whether or not anyone will actually use it like:

const myString = String.cooked`Some ${templated} string`;

The signature of the function must be that of a template tag for some of the most desirable use cases. That means it is a valid tag, and as such should make sense of for some reason someone did use it as one. And at the same time, it needs to make sense when delegated to within a custom tag, whether the custom tag is just getting the cooked string and doing something else with it, or actually modifying the interpolation behavior.

That is, regardless of how the function is used, it should not have a confusing name.

bakkot commented 4 weeks ago

The strings parts of your template won't ever have to be sanitised, it's your source code! Only values part may have to be sanitized...

The appropriate way to sanitize each part may depend on the context of the string. You can't generally take something which operates on a template literal and transform it into something which operates only on a single string - the surrounding context is an important part of the logic. (Consider sanitizing HTML, for example: the rules for escaping attribute values are not the same as the rules for escaping element contents.) Sanitizing each part individually, without the surrounding context, isn't generally possible.

So, yes, the function does in fact really need to be used as a tag directly.

devingfx commented 4 weeks ago

@zeel01 has you said earlier, the difficult task is to concile 2 usages that are so different... It is why I argued on the side: It wont be used as a tag so the name should reflect that...

That being said, I don't want to spam this thread anymore, I apologize for this BTW ! Other contributors will argue, I may change my mind later :)

devingfx commented 4 weeks ago

A last philosophic comment:

We, as developpers, and here, as specifiers are responsible of the consequences of choices we take. More that final users who will use what is available.

If the simple choice of naming a future fonction would lead to a usage that kills performances for a matter of lazyness, we would have be responsible of more worldwide energy comsomption therefore more CO2 emissions in a world where global climate change matters, and where sobriety should be the path to take...

zeel01 commented 4 weeks ago

I would hope bundlers/optimizers would remove completely no-op uses. Heck, I would hope that runtimes would be smart and realize a tag doesn't do anything and just not call it. I wouldn't worry about a hypothetical extra function call, there are many ways to avoid any runtime performance penalty without the developer needing to jump through as many hoops authoring the code.