Why doesn't state behave like the documentation says?

I’am trying to get my head wrapped around state, so I made this little document:

#let st = state("test_state", 0)

#let show_state(state) = [
   State = #context(state.get())
]

Initial state: #show_state(st)

#st.update(1)  // Why no output here?
#st.update(2)

After update, expect 2: #show_state(st)

#st.update(c => c + 1)
#st.update(c => c + 2)

Final state, expect 4: #show_state(st)

So, the documentation for state.update mentions that update will return content, but it doesn’t return anything. It also mentions that an update will not be applied if the result is never shown, but it seems that all updates are applied sequentially. I’m confused.

Well, that’s the ingenious/tricky thing. state.update returns content, but what it returns is not visible. Think of it as an invisible token or cookie that represents the state update.

What Typst is trying to tell you is that state.update returns a sliver of content that you must include in the document’s content otherwise the state update won’t happen at all. Updates happen in the order and location that these “cookies” appear in the document…

I think you seem to have a handle on it. Maybe only that the final expected value should be 5 because before the final state you have done both +1 and +2 to the state, so the final value should be 2 + 1 + 2 = 5

My state explanation would sound like this: Using state for Caching in Typst — Is It Possible? - #3 by bluss


The next thing you can try is making an update but not inserting it, it will not affect the final count:

#let my-update = st.update(c => c + 1)

Then insert your update twice, and it will affect the final count (twice):

#my-update #my-update
4 Likes

OK, I understand now. The documentation really reads backwards for me. It made me feel as if it is necessary to follow any use of update() with a context(get()) to make the update stick.

IMHO, the documentation should emphasise that the function returns invisible content, like an anchor, to indicate where it should apply the update, and that you need to include this return value in the document. It should stress that just writing #state("statename").update(value) includes this invisible anchor in the document stream, which makes it do what you want in the vast mayority of the cases.

It may point out that you can go out of the way to make Typst forget about this anchor by using this special construct #let _ = state("statename").update(value) and never using _ anywhere. But it shouldn’t present that as a normal thing to do.

#let st = state("test_state", 0)

#let show_state(state) = [
   State = #context(state.get())
]

Initial state: #show_state(st)

#st.update(1)  // Invisible return gets inserted into document
#st.update(2)  // This update overwrites the previous one

After update, expect 2: #show_state(st)

#st.update(c => c + 1)
#st.update(c => c + 2)

Final state, expect 5: #show_state(st)

#let _ = st.update(c => c + 1) // We throw away this invisible return 
#st.update(c => c + 2)

Really final state, expect 7: #show_state(st)

See Improve documentation for state.update by Andrew15-5 · Pull Request #7192 · typst/typst · GitHub.

It says

State updates are always applied in layout order and in that case, Typst wouldn’t know when to update the state.

Well, the new docs already have an example that doesn’t print the updated value, though it still retrieves it, to show the new styling. An example that updates state and doesn’t show its effect is not a visually good representation of what’s going on, IMO.

1 Like

Thank you very much for the documentation change. I was looking around but couldn’t find the reference in typst/doc/ . However, I do feel that your update still puts too much emphasis on the unusual case.

I tried my hand at explaining it this way:

self.update (any function) -> content

Set the value of the state to any or update the value of the state according to the function. function receives the old value of the state. It must return the new value of the state.

Example:

#let st = state("test_state", 0)

#let double(i) = {i * 2}

The initial value of our state is: #context(st.get()). // 0

#st.update(7)

The value of the state is now: #context(st.get()). // 7

#st.update(double)

And now, the value of the state is: #context(st.get()). // 14

Note that update returns an invisible marker that normally gets inserted straight into the document, as in the example above. This invisible marker tells Typst that, at this location, it should perform this update. That’ s because Typst apllies all updates in the order they appear in the final document (layout order), not necessary the source order. See above. (reference to the beginning of the state reference documentation)

If you want to, you can save this invisible marker for later use. This means that the update will not yet be applied. Continuuing the example:


#let doubler = st.update(double) // not inserted yet

The value is unchanged, despite the update: #context(st.get()).   // 14

#doubler  // insert the invisible marker 

But now, the update has been applied. The state is: #context(st.get()). // 28

#doubler  // insert the invisible marker again

We can apply the same update twice! State is now: #context(st.get()). // 56

1 Like

I don’t think suddenly too much function docs is good, so I’ll leave it to @laurmaedje.

Just write #let double(i) = i * 2.

The initial value of our state is: #context st.get(). // 0