Hello. In Typst, current it’s not possible to define structs.
However, there are built-in types which have modifiable states: for example, arrays and dictionaries. As long as they are in scope, their internal state could be modified.
I’m writing a simple package to help creating simple exercise sheets with solutions in separated pages:
#let elementary-exercises-init(key: "elementary-exercises") = {
/*
Format:
exercises := (
(
number: int | default auto,
statement: content,
answer: content
)*
)
*/
let exercises-state = state(key, ())
let add-exercise(number: auto, statement, ..answer-maybe) = {
let answer = answer-maybe.at(0, default: none)
exercises-state.update(exercises => {
exercises.push((number: auto, statement: statement, answer: answer))
exercises
})
}
let resolve-auto(exercises) = {
let current-number = 0
let exercises-result = ()
for exercise in exercises {
exercise.insert(
"number",
{
if exercise.at("number") == auto {
current-number += 1
} else {
current-number = exercise.at("number")
}
current-number
}
)
exercises-result.push(exercise)
}
exercises-result
}
let show-statements = [
#context {
let exercises = exercises-state.get()
exercises = resolve-auto(exercises)
let items = exercises.map(
exercise => enum.item(exercise.at("number"), {
exercise.at("statement")
v(1em)
})
)
enum(..items)
}
]
let show-answers = [
#context {
let exercises = exercises-state.get()
exercises = resolve-auto(exercises)
let items = exercises.map(
exercise => {
if exercise.at("answer") != none {
enum.item(exercise.at("number"), {
exercise.at("answer")
v(1em)
})
} else {
none
}
}
).filter(x => x != none)
enum(..items)
}
]
(add-exercise, show-statements, show-answers)
}
#let (add-exercise, show-statements, show-answers) = elementary-exercises-init()
However, this implementation uses context, and I believe it’s not ideally elegant. There are some issues with state:
Accessing it requires context
They aren’t scoped
I believe a more elegant solution is to allow user-defined structs, or allow adding methods to Javascript-like objects. Within such objects, there can be mutable states. It mimics the idea of Haskell’s ST monad. Since arrays and dictionaries are build-in with such features, I believe allowing user-defined objects won’t violate the functional design of Typst.
user-defined types areplanned, which would give us structs with methods on them. Elembic is a package that simulates custom types, but more in the “styling custom elements” sense, less in the “general purpose structs” sense.
However, custom types would not automatically give us mutating methods (or functions). I actually proposed wished for this here:
One reaction was
@ensko fancy stuff, but I dread the idea of having to read and understand such code
I largely agree, although limiting this feature to custom types similar to what arrays and dicts do may be enough to make it simple enough to understand. The really important point from my perspective is that there really isn’t too much going on semantically; it can be all put in terms of a simple syntactic sugar, and tackled independently once we have custom types.
For now, you can take suiji as inspiration and write code in the “explicitly return the modified struct” fashion if you want/need to avoid state/context.
I don’t know if we had too much state vs query discussion, but the more I do with Typst, the more I think metadata + query are superior to state. Either state or metadata + query can solve about the same tasks, but the latter is more often easier to work with. Probably because it more explicitly embraces Typst’s model and that any “state” that you keep is ultimately stored as parts of the document.
In your example, I would say for now, prefer query and not state. Then add-exercise adds an (invisible) exercise metadata to the document instead of using state, and it continues like that, instead of state.get() you have queries to fetch your combined exercises.
You don’t have to handle arrays or data structures inside state that way - multiple exercises are just a consequence of query returning an array. The end result is easier to work with.
Also, small tip, since exercise is a dictionary, ease your typing by replacing exercise.at("number") with exercise.number and so on.
(I hope this is not too far off topic - your topic is custom types, which makes sense - but I also wanted to address the example code in the post.)
Hello! The issue of metadata + query is, sometimes people want to have multiple independent threads of states. For example, they might want an independent exercise sheet at the end of each chapter. It also fits the idea of functional programming better. If we use metadata for this, there’s another burden of generating a different handle in the metadata each time. The same issue occurs when people want multiple reference sections.
Hello! This method is valid, but there is an issue. If some user is using suiji, then they is likely a developer. However if someone is using an exercise sheet template, then they could very possibly be a normie unfamiliar with programming, and this usage would scare them off.