Skip to content
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

Constants in markup #33

Merged
merged 2 commits into from
Jun 26, 2021
Merged

Constants in markup #33

merged 2 commits into from
Jun 26, 2021

Conversation

Rich-Harris
Copy link
Member

@Rich-Harris Rich-Harris commented Sep 10, 2020

@benmccann
Copy link
Member

benmccann commented Sep 10, 2020

I could imagine people putting a more complex expression in an @const than we typically find in svelte expressions today, which might create more demand for those blocks to have TypeScript support, which I don't think they have now. (CC @dummdidumm)

@lukeed
Copy link
Member

lukeed commented Sep 10, 2020

Do these blocks allow for reactive assignments?

@tivac
Copy link

tivac commented Sep 10, 2020

Is @const the best keyword?

Yes.

Should we allow multiple declarations per tag? (Probably not.)

No.

Does TDZ apply?

No.

Are declarations ordered as-authored, or topologically?

Topologically so they work like reactive statements.

@dummdidumm
Copy link
Member

I could imagine people putting a more complex expression in an @const than we typically find in svelte expressions today, which might create more demand for those blocks to have TypeScript support, which I don't think they have now.

This might happen yes. And yes, template tags cannot have TS right now. But I think we should look at this separately, since I think this is a good addition either way.

About TDZ: I would say it should apply, at least for me it would be more confusing to use something like this first and then declare it. So we would force the user to write more readable code.

@tanhauhau
Copy link
Member

how are we handling circular dependencies?

{@const a = a + 1}

or

{@const a = b}
{@const b = a + a}

@tanhauhau
Copy link
Member

Also related: sveltejs/svelte#4601

@Conduitry
Copy link
Member

I was just coming here to comment about sveltejs/svelte#4601

One of the main differences with that implementation is that the scope of the declared variable is much more explicit.

@Rich-Harris
Copy link
Member Author

Do these blocks allow for reactive assignments?

@lukeed They're read-only, but values update when their dependencies do

how are we handling circular dependencies?

@tanhauhau if we use topological ordering, the same way as reactive declarations currently do. if not, then we can rely on simple TDZ logic (even if we don't insist that declarations happen before usage, applying TDZ after hoisting declarations in the order in which they were written would handle circularity)

Had forgotten about sveltejs/svelte#4601, thanks. I'm more inclined towards an {@ tag than a {# tag because the indentation could quickly get out of control — this example from the RFC...

{#if n}
  <p>{n}^4 = {hypercubed}</p>

  {@const squared = n * n}
  {@const cubed = squared * n}
  {@const hypercubed = cubed * n}
{/if}

...would look like this:

{#if n}
  {#with n * n as squared}
    {#with squared * n as cubed}
      {#with cubed * n as hypercubed}
        <p>{n}^4 = {hypercubed}</p>
      {/with}
    {/with}
  {/with}
{/if}

One of the main differences with that implementation is that the scope of the declared variable is much more explicit.

I'm not too concerned about this — in practical terms I don't think it's any less clear, in the same way that this...

if (foo) {
  const total = a + b;
  console.log(`${a} + ${b} is ${total}`);
}

...is just as easily understood as this if not more so:

if (foo) {
  with ({ total: a + b }) {
    console.log(`${a} + ${b} is ${total}`);
  }
}

@TehShrike
Copy link
Member

This seems solid to me. If you want to thumb your nose in the direction of the whole "everyone is confused about the difference between const variables and const values" thing you could go with @final, but that's probably a tough argument to make very seriously :-P

@pngwn
Copy link
Member

pngwn commented Sep 10, 2020

I am not a fan of this for the simple reason that it moves even more logic out of the script block and into the template. Currently, Svelte strongly encourages users to put most of their logic in the script block. This has the value that when I come across a component I can scan the script to see the bulk of the logic. Logic inside the template itself is both limited and easy to spot due to the block syntax. One of my big criticisms of JSX is that it encourages users to put significant amount of logic in the 'template' instead of separating things out. With SVelte it doesn't matter who wrote it, i tend to see more consistencies in how people write their code. 'Onboarding' complexity is reduced because everything is roughly where I expect it to be. This would work against that to a degree.

In terms of 'doing the same thing twice': using the function example and wrapping it in a memoise helper would actually solve this problem and give you more besides, something that this proposal doesn't do. That should probably be listed as an alternative. If memory is a concern in that case (due to looping thousands of times), then using a memoise function with a simple LRU cache of a few elements would prevent the additional computation and keep the memory overhead low.

I'm mainly not a fan of this because it adds additional syntax for something that can already be solved in user-land (albeit with a little function call + memory overhead), it encourages putting even more logic into the component template which can make components difficult to reason about, and it also allows for more divergence in how people write there code and where they put their logic, making different svelte codebases potentially even more different due to fewer constraints. This last point is actually something I really value, I read a lot of Svelte code by a lot of different people and broadly speaking things look the same and are in the same places.

@antony
Copy link
Member

antony commented Sep 10, 2020

Also a vote against, for the simple reason that logicless templates would be the ultimate goal for me. I'm even against the await block we have in templates. I think @pngwn covered the rest.

@dummdidumm
Copy link
Member

dummdidumm commented Sep 10, 2020

About the argument against it, "{@const will make code less consistent ": I think the same is true now, since people can come up with very different ways of dealing with the "computed value inside each loop/if function" problem. Some extract components, some use functions, some will prepare the array differently beforehand.

I also think this would be great in terms of colocation. If I need some intermediate result I no longer have to jump between script and markup.

But of course this could be overused.

@pngwn
Copy link
Member

pngwn commented Sep 10, 2020

But you always know where the logic will be. Either it is defined where the value is used or it is in the script. There are no other options.

@PaulMaly
Copy link

PaulMaly commented Sep 10, 2020

Agree with @pngwn and @antony. I believe this feature is overrated in RFC. Cons aren't covered by pros. Alternatives are fine.

But,

seems this feature mostly makes sense inside {#each} tag. For the single value, we can just use the reactive declaration to compute it in a script. And the main con is about potential over-usage of logic inside a template. So, maybe, we can just extend a syntax of {#each} tag to support additional block-scoped variables? So, we limit its usage only inside {#each} tag, where we exactly need it.

I'm not sure how exactly it should look like, but maybe something like:

{#each 
  boxes as box, 
  index, 
  area = box.width * box.height
}
  <div
    class="box"
    class:large={area >= 10000}
    style="width: {box.width}px; height: {box.height}px"
  >
    {box.width} * {box.height} = {area}
  </div>
{/each}

@Rich-Harris what do you think about this counter-proposal? ))))

@knobo
Copy link

knobo commented Sep 10, 2020

I like this, mostly because it allows me to write small components without creating another separate sub-component for holding the value simple computation. I get annoyed every time I need to create a component just to hold a variable, or even move the computation away from the relevant location. It reminds me of the days where variables in C had to be declared at the top of the function.

It might not be the academically correct way of creating a template language (or maybe it is?), but it is at least in the line of getting things done.

@ryanatkn
Copy link

ryanatkn commented Sep 10, 2020

I agree with the objections of @pngwn and others for most of the use cases I have, but this RFC addresses a significant pain point I've had with {#each} blocks doing data visualization and graphics. Often these:

  • can have many local temporary named variables which aid readability, writability, and performance
  • can be nested inside multiple {#each} blocks that conceptually have their own locally scoped variables, compounding the problem
  • are performance sensitive
  • are much better to read and write as inline anonymous templates, not components

@PaulMaly's suggestion would solve my primary use case. An alternative (maybe not good) would be to restrict {@const} to certain blocks like {#each} and {#if}. In both cases, it significantly reduces the "multiple ways to do the same thing" problem and avoids ergonomic and performance overhead of our current situation.

@lukeed
Copy link
Member

lukeed commented Sep 10, 2020

Agree with all counterpoints.

The only places where I think something like this would be permissible would be:

The latter two are more questionable, though, since they have access to the component's script contents, allowing for whatever memoize/helper functions.

WRT inline components (IC): If they inherit the parent's full context (#34 (comment)) then that removes inline components from my list above, meaning there's no justification for {@const} anywhere. Only if IC are fully encapsulated will {@const} have a single purpose. At that point, compiler should disallow its usage anywhere except within <svelte:template> tags.

@arxpoetica
Copy link
Member

arxpoetica commented Sep 10, 2020

Of the three proposals rendered today, this is the one I want the most. Arguably, it leans into JSX land—including logic in the templates.

Counter argument: one of the reasons people sometimes balk at mustache-like syntax is just that: logic in the templates. I'm persuaded that co-locating just this one type of logic will be useful. It's certainly something I've wanted to reach for prior, but always end up with a boilerplate function somewhere, which feels hacky.

I'm also persuaded by the arguments that it will be easier to track stuff down by co-location.

@lukeed inline components, await...then, and each blocks are the exact use case where I want this.

@lukeed
Copy link
Member

lukeed commented Sep 10, 2020

@arxpoetica Right, because those are the only 3 instances where new "scopes" are created, which means you're seeing data for (probably) the first time. That said, each and await can still access helpers inside the parent scope – and whether or not inline components can is still TBD.

@arxpoetica
Copy link
Member

can still access helpers inside the parent scope

This is exactly why I'm in favor of @const. Helpers are fine, except when they're not and turn into boilerplate.

@Rich-Harris
Copy link
Member Author

because those are the only 3 instances where new "scopes" are created

There's also <Foo let:bar>

@stalkerg
Copy link

stalkerg commented Sep 11, 2020

Is @const the best keyword?

Yes.

Should we allow multiple declarations per tag? (Probably not.)

not understand this question

Does TDZ apply?

yes

Are declarations ordered as-authored, or topologically?

yes

What I want to say - this is very useful and similar to what Mako template has.
For example, in a few of my projects, I can remove components because they needed only to reduce extra calculations in a cycle.
At the same time, I agree with @PaulMaly this feature has a sense only in a cycle. Maybe, instead very abstract and flexible {@const we should somehow extend {%each

@stephane-vanraes
Copy link

In the motivation for #32 you mention

Since re-rendering in Svelte happens at a more granular level than the component, there is no artificial pressure to create smaller components than would be naturally desirable, and in fact (because one-component-per-file) there is pressure in the opposite direction. As such, large components are not uncommon.

But I feel like this proposal removes even more of this 'artificial pressure'.

Taking all three of today's proposals (#32 #33 #34) together it would become theoretically possible to make an entire application in a single file which is the complete opposite direction of what Svelte has been so far.

@stalkerg
Copy link

stalkerg commented Sep 11, 2020

@stephane-vanraes hmm looks like if we create a new branch or scope for some corner cases, we should have full features of a component without creating a new file. The main question of how to solve these cases without making a bomb for common behavior. We are very close to full inline components (with css, js logic, and templates).

PS idea for syntax:
We can introduce a new tag for extra each/await options? like

{#each boxes as box}
{-values a=box.x*box.y}
...
{/each}

@qm3ster
Copy link

qm3ster commented Sep 25, 2020

This seems crucial, for example for #if inside #each

{#each boxes as box}
  {@const b = box?.a?.b}
  {#if b}
    <p>{b}</p>
  {/if}
{/each}

@hm-lee
Copy link

hm-lee commented Sep 25, 2020

@Rich-Harris

What about @memo instead of @const?
Is it necessary to confine this to const value?

@const is a familiar naming, but it gives the impression that the value will not change.
I think it would be better if we could name it some kinds of
inline memoization and expand the use of it as an easy-to-use macro.

@pngwn
Copy link
Member

pngwn commented Sep 25, 2020

@memo implies memoisation which this is not. We could just go with @var instead.

@hm-lee
Copy link

hm-lee commented Sep 25, 2020

@pngwn I think this potentially has a memoization role.
If may not, I think a @const is more appropriate than a @var or a @let.

@pngwn
Copy link
Member

pngwn commented Sep 25, 2020

That certainly isn't part of this proposal and would introduce a significant amount of complexity.

@Conduitry
Copy link
Member

Memoization would be another way to achieve a similar end result as this feature, but this isn't memoization.

@hm-lee
Copy link

hm-lee commented Sep 25, 2020

Well, then, @const seems the most appropriate.

@qm3ster
Copy link

qm3ster commented Sep 25, 2020

RE: const vs var vs let

{#each [1, 2, 3] as x}
  <p>{(x *= 4)} {(x *= x)} {(x *= x)}</p>
{/each}

this currently works.

But I don't think we should encourage it with naming.

image

@qm3ster
Copy link

qm3ster commented Oct 18, 2020

Is there an implementation branch? Possibly @RedHatter can adapt from sveltejs/svelte#4601?

@TylerRick
Copy link

TylerRick commented Oct 19, 2020

I keep running into cases where this would be useful. At first it seemed like {#each} was the case where it would be most useful, but now I have to say that I think they would be just as useful for use with slots (<Component let:object>).

I also hope that destructuring will be supported. I assume it would be; I just don't see any specific mention of it here yet (looks like it already is in sveltejs/svelte#4601).

  {@const {submitting, pristine, values} = state}

My latest use case: I want to both make use of object destructuring (so that I can use nice short names for things that I will be referencing a lot within the block) and still be able to pass on the full object as needed, like this:

  <Form let:state>
    {@const {submitting, values, errors} = state}
    … <!-- (make use of submitting, values, etc.) -->
    <Something {state} />
  </Form>

Then you can have it both ways, reduce duplication, — and take advantage of the let:var shorthand!

If you start out by destructuring:

  <Form let:state={{submitting, values, errors}}>
    … <!-- (make use of submitting, values, etc.) -->
  </Form>

and then later realize that you need the entire object for something, you can't simply pass it through like:

    <!-- (pass along entire contents of let:state ... can't!) -->
    <Something {state} />

without rewriting the rest of your block to use long-handed state.values, state.errors, etc.

Sure, you could try to recombine the props like this:

    <Something state={submitting, values, errors}} />

But that's duplication and only works if the destructuring lists all possible keys of the object — which it shouldn't have to know about in order to cherry pick some of them for convenient referencing within the current block. Otherwise, any keys that were omitted from the destructuring would get "permanently" lost, since we don't have access to the original object.

I tried to work around is like this:

  <Form let:state={{submitting, values, errors, ...rest}}>
    …
    <Something state={submitting, values, errors, ...rest}} />

but then you run into this error:

  let directive value must be an identifier or an object/array pattern svelte(invalid-let)

... which is why I'm specifically mentioning the let:var case for consideration here.

@TylerRick
Copy link

TylerRick commented Oct 19, 2020

I also want to say that the advantages of co-location — being able to put related code that is used together (particularly when that is the only place it is used) close together in the source code — seems (to me at least) like a far more compelling and pragmatic concern than concerns about logic-less templates or {@const} making Svelte code less consistent between authors/projects. Logic in templates (such as #{if} and #{each} that we already have) can do just as much or more to increase readability and maintainability as to decrease it. The expressiveness of Svelte's templates are one of its greatest strengths.

The other advantage of being able to closely co-locate constants with where those constants are used is that it allows you to more easily experiment with the idea of possibly extracting a new component; its lets you start "bringing together" all the pieces that a section of template depends on, in preparation for later extraction.

Coming from React, one of the things I miss the most is how trivially easy it was to extract an inline function component out of any part of your component that started to look like it belonged in its own component, or was being duplicated in several places in your component. Just start with const Something => () => <whatever>...</whatever>, making use of variables from closure at first, and then over time, define a more explicit interface where all the data that it uses is passed in via props. Finally, when your new component has grown enough legs, and you've confirmed in your mind that it should stand on its own, you can trivially move your inline component to a new file. (Or your experiment might prove to you that it's too closely coupled to your main component and you leave it there after all...)

Being able to easily define reactive constants closer to where they are used brings Svelte one step closer to being able to easily prepare/stage code for possible extraction into a new component.

@Rich-Harris mentioned this before, which I resonated with:

Since re-rendering in Svelte happens at a more granular level than the component, there is no artificial pressure to create smaller components than would be naturally desirable, and in fact (because one-component-per-file) there is pressure in the opposite direction. As such, large components are not uncommon.

Because of that, it's easy to end up in a situation where the styles for a given piece of markup are defined far away (in terms of number of lines) from the markup itself, which reduces the advantage of having styles and markup co-located in the first place.

When a component reaches such a size that this becomes a problem, the obvious course of action is to refactor it into multiple components. But the refactoring is complex for the same reason: extracting the styles that relate to a particular piece of markup is an error-prone manual process, where the relevant styles may be interleaved with irrelevant ones.

and from the inline-components RFC:

Together with the RFCs for local scoped styles and constants in markup, this gives us everything we need to create self-contained logic-less components, even including slotted content:

<svelte:template name="tooltip" let:dangerousness>
  {@const danger = dangerousness > 5}

  <div class="tooltip" class:danger>
    <slot></slot>
  </div>
  ...

I also resonated with @dummdidumm's comments:

About the argument against it, "{@const} will make code less consistent ": I think the same is true now, since people can come up with very different ways of dealing with the "computed value inside each loop/if function" problem. Some extract components, some use functions, some will prepare the array differently beforehand.

I also think this would be great in terms of colocation. If I need some intermediate result I no longer have to jump between script and markup.

and @knobo's:

One of Svelte's advantages, for me, is that I can test out ideas with relatively few lines of code. the #with feature could save me from adding a separate component for the content of an #each loop.

I get frustrated when I have to create a new file, move the content of the #each clause, import it as a component, and add attributes and create exports for that, and implement events to send messages back, and event handlers, when I just wanted to test a small feature.

When I'm prototyping components I like to manage the data where it appears, and not send it back and forth if there is no reason for it. I also don't like to be forced by a language to do things a certain way.

@pngwn
Copy link
Member

pngwn commented Oct 23, 2020

I have changed my mind. I'm in favour of this proposal (especially in conjunction with the inline component proposal).

I think the main issue here is simplicity. Yes, you can do this with temporary variables or memoisation but temps vars have to live in the script tag making the code more difficult to reason about and involve lots of jumping around. In the case of using memoisation to solve this problem: this is a poorly understood concept with a huge potential for complexity, memoisation as a solution means you need to think about caching and @TehShrike has convinced me that this is an unreasonable expectation, especially for simpler cases where @const would be useful and frequently used.

I generally think that @const will open up a bunch of possibilities to a wide audience without needing to implement relatively complex solutions and offers a nice convenience method in place of those approaches (that people are probably using today). As mentioned by @dummdidumm, colocating the temporary variable with the block that utilises it also has its advantages.

@lukaszpolowczyk
Copy link

I thought using mapping was a good option. I do this to use ready-made mapped properties that you just need to place in the right place in the component.

But using @const in an inline component makes sense.
Although previously I thought you should be able to put script in an inline component, I changed my mind.

@const in inline component is ok. Maybe also in #each and #await.

@lukaszpolowczyk
Copy link

Suggestion: {@const} inside a tag.

The constant would be available in each mustache used in the tag, e.g .:

<svelte:window 
  {@const val = {parm1,parm2}}
  on:keydown={()=>doSometing(val)}
  on:keyup={()=>doSometingElse(val)}
/>

But it won't be available outside of the tag.

@sysmat
Copy link

sysmat commented Apr 29, 2021

any progress on this?

  • I'm new to svelte, something like that would be nice
<script lang="ts">
 const func1(val: MyType): MyType2 => {
   // do something with val
   return  MyType2 ;
 }
</script >
{#directive my1}
  {@set my2 = func1(my1)}
  {my2.prop}
{/directive}

@Rich-Harris
Copy link
Member Author

Merged after discussion with the core team — PR exists here sveltejs/svelte#6413

@knobo
Copy link

knobo commented Jun 28, 2021

it would be interesting with a summary of the discussion. (or at least the hightlights)

@pngwn
Copy link
Member

pngwn commented Jun 28, 2021

Was basically a combination of the benefits pointed out in the original RFC and this comment which are counter-arguments to my original rejection.

@lukaszpolowczyk
Copy link

Would {@const val = await getURL()} be acceptable?

Would often be sufficient and neater than a whole big {#await}.

@tv42
Copy link

tv42 commented Jan 12, 2022

For anyone else stumbling here trying to understand what @const is and why things are acting weird around it, right now using @const breaks prettify: sveltejs/prettier-plugin-svelte#272

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet