Can I attach and update state in metadata?

I have some global state that I want to be able to query. Multiple files contribute their data to this state, and each file gets its own label. So, it would be natural to put all the files’ state into their own metadata blocks, query them by label, and update them when needed.

I tried the following, but the state isn’t updated.

#metadata(state("test", 0)) <test>
#context query(<test>).first().value.update(cs => {
  cs += 1
})
#context query(<test>)

Outputs: (metadata(value: state("test", 0)),).

Careful, there are a few things in your post that make me think you have the wrong mental model.

first, the update() method. This works by returning a new value based on the previous one. However, you try to modify the parameter:

// BAD
#state("test", 0).update(cs => {
  // this statement has no result, only a side effect
  // its result is thus implicitly none
  cs += 1
  // the block ends here, returning the last expression's value
  // instead of incrementing, the state is now none
})

// GOOD
#state("test", 0).update(cs => {
  cs + 1
  // the block ends here, returning 1
})

second, you’re not actually retrieving the state’s current value:

// BAD: you're just looking up the metadata
#context query(<test>)
// gives (metadata(value: state("test", 0)),)

// BETTER: extracting the state out of the metadata
#context query(<test>).first().value
// gives state("test", 0)

// GOOD
#context query(<test>).first().value.get()
// gives the actual state value

the state itself is not modified by an update, thus the “BETTER” code always has a 0 inside: that’s how the state was set up. Only with get() do you extract the value.

Finally, both states and labels have globally unique names, so putting state inside labelled metadata doesn’t bring much benefit. Also, state is only identified by the name:

#metadata(state("test", 0)) <test>
#metadata(state("test", 0)) <test2>
#context query(<test>).first().value.update(cs => cs + 1)

#context query(<test2>).first().value.get()
// prints 1, because the "test" state was updated

Thus, I’d recommend to just replace labels with state names, that makes more sense.

One final note on something peculiar – consider this:

#metadata(state("test", 0)) <test>
#metadata(state("test", 1)) <test2>
#context query(<test>).first().value.update(cs => cs + 1)

#context query(<test2>).first().value.get()
// prints 2, because the "test" state was updated from its initial value of 1

state updates affect all state “objects” of the same name, but the initial value of the specific object matters! It’s therefore not recommended to create multiple different states. My recommendation is to put each state in a variable and import that variable.

2 Likes

Thanks for taking the time to explain all the little details.

#state("test", 0).update(cs => cs += 1)
// Wouldn't even make sense, because if we don't use a function we would do, for example:
#state("test", 0).update(1)

Is the following description of states accurate? States themselves never change, but instead we put down markers (which live in the content) that are used to compute what the value of state at a given position should be. The markers that we put down only have the name of the state and the desired operation. When we call state.get(), we’re taking that state’s initial value, applying all the markers that match its name that we’ve seen thus far, and giving the result.

I have a related question, then, about another function of metadata. Since the contained state itself isn’t updated, is there a way to query the final value of a state from the CLI?

1 Like

Your description is spot-on! the state itself is immutable, and get() collects all update() contents up to that point.

I think this would make a decent question on its own that could help people in the future, would you mind opening a new question with a fitting title for it?

1 Like

Great, thank you! I’ll go ahead and open a new question.

I will link How to create invisible label? - #8 by Andrew to again mention that cs =+ 1 will return none and that for integer number counting using counter over state is the preferred way. And subsequently, all counters are initialized with 0 value, and therefore you don’t need to explicitly set it in any way.