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

Ability to freeze state #1841

Open
edgarogh opened this issue Aug 1, 2023 · 10 comments
Open

Ability to freeze state #1841

edgarogh opened this issue Aug 1, 2023 · 10 comments
Labels
feature request New feature or request introspection Related to introspection. styling About set and show rules or style properties

Comments

@edgarogh
Copy link

edgarogh commented Aug 1, 2023

Context / use-case

Slideshow with "fragments" or "pauses"

I would like to create a template for PDF slideshows, with a #fragment[] function that allows me to hide an element at first and reveal it later on a duplicate page (like reveal.js fragments or Beamer's \pauses).

Student quizz / multiple choice question with permuted answers

Described later here (scroll).

Description

To do so, I'd need the ability to duplicate a page, freezing almost all state (including state() variables, heading numbering, page number, etc.), except for one "iteration variable" that tells me which copy I'm laying out currently.

Essentially, I'm looking for a function that would "instantiate" a content at a specific location (regarding state) in a way that resolves/freezes implicit and explicit state inside it, so that any attempt to lay it out multiple times results in the same state materialization.

Possible implementation 1: freeze(content, exclude: list or state)

#let idx = state("idx", 0)
#let body = [= Heading #idx.display() #idx.update(v => v 1)] // depends on implicit heading state

#let body_frozen = freeze(body, exclude: (idx,))

// Headings have the same number
body_frozen
body_frozen
body_frozen

Here, we explicitly exclude a state variable from the freezing, so it can be used and mutated inside.

Note: if a heading is inserted between the call to freeze and the layouts of body_frozen (L5), we would expect the numbering to be in the wrong order, which I guess would be a feature?

Possible implementation 2: clone(content, count: number, exclude: list or state)

This approach would be more "managed" and tailored to my specific case, leading to less room for weird behavior such as unordered numbering:

#let idx = state("idx", 0)
#let body = [= Heading #idx.display() #idx.update(v => v 1)]

clone(body, exclude: (idx,), count: 3) // Creates 3 copies of body, with only "idx" being mutated

Another possible approach would be to provide an map-like usage:

#let idx = state("idx", 0)
#let body = [= Heading #idx.display()]

clone(body, list: (0, 1, 2), state: idx)

Here, the list elements would be mapped to the body, passing their values through the idx state variable. The state variable could maybe have its original value stored and restored after the iteration, so that it isn't observable outside?

Soundness

I must admit, I haven't thought that much about the "soundness" of such a feature, and how it would interact with all the weird "meta" Typst features (locate, query, etc.).

Last note

Maybe this idea is a bad solution to the project I have initially (XY problem). Maybe it should be restricted in other ways. Or made even more generic. Maybe my initial problem should just be a Typst builtin.

@sitandr
Copy link
Contributor

sitandr commented Aug 1, 2023

Have you looked into polylux package? It seems like a complete solution to your task.

@edgarogh
Copy link
Author

edgarogh commented Aug 2, 2023

I don't know how I didn't find about Polylux when Googling but that's do the job perfectly!


However, it does indeed look like Polylux suffers from the exact problem I'm describing: if you add numbered headings to your slides, their number increases on each sub-slide, when you'd expect the slide to remain the same overall (apart from the overlays).

Here's an example: https://typst.app/project/rxemPv9LmpjiDKVKTESxnb

@PgBiel
Copy link
Contributor

PgBiel commented Aug 3, 2023

As a workaround, you can likely store the original location in some write-once state and use it to retrieve counters with .at(loc), and, with that, build a custom "heading" (maybe having a real one show up only on the first slide for referencing purposes). Not a very optimal solution, though.

Truth is, it would be weird to have multiple headings with the same number, as references "wouldn't work". But I think some sort of middle ground can be found here (maybe by only pointing to the first one, for example - although that might seem arbitrary).

@sitandr
Copy link
Contributor

sitandr commented Aug 3, 2023

I'm afraid there is mo easy solution that can solve that issue via some let or show issues. So for complex things (that can't be solved by using polylux counters for slides and subslides) to make it viable you need to go inside polylux code.

Maybe it is worth trying to make an issue in Polylux repo, so the could decide what is the best fitting missing consistent Typst feature. It is easy to store the state for some given counters, but impossible for all automatically. Maybe we should add some exporting all the states into dicts and back, maybe something entirely else. I don't know what is the best solution there.

@ntjess
Copy link

ntjess commented Aug 9, 2023

I realize that https://discord.com/channels/1054443721975922748/1138661247940821146 might solve this issue as well -- If content could be exported to an svg string, you can simply recall the frozen state with image.decode(svg-str)

Pasting the question for reference:

Many publishers require you to provide each figure as a separate file along with your text. With figures made in typst, this is difficult to isolate since outer scope show rules will change the behavior if drawn figures are pasted elsewhere. With the merger of #1729, would the mechanisms be in place to, say, let svg-str = export-svg(content)?

The cli query system can then pull those svg strings and write them to a file, if they are e.g. saved to a metadata field

@edgarogh
Copy link
Author

I'm coming back with another use-case that I randomly stumbled upon: printing student quizzes (MCQ) with randomised answer order.

Example use

Assume that template and question are defined. The following code creates a 30 page PDF containing the answer sheets of a multiple-choice question exam for 30 students. All of them have almost the same content, with the exception of question answers, which are pseudo-randomly permuted to make cheating harder.

#show template.with(student-count: 30)

= Exam

#question(
  "What's the first letter of the alphabet?",
  right: "A",
  wrong: ("B", "C"),
)

To implement template(), you need to repeat the same content 30 times with just the PRNG seed being different. You don't want any title numbering or question numbering cascading to the following sheet. There are currently two solutions:

  • Manually reset any state or counter after each answer sheet. This is tedious, especially when counters and state from packages are involved.
  • Generate each answer sheet individually and merge them after. You loose a lot of performance on process invocation, PDF parsing, PDF encoding, and lack of memoization accross each student sheet. Also, any call to datetime.now() may result in different values between each answer sheet.

The system I describe in this issue could solve this problem very trivially.

@laurmaedje laurmaedje added feature request New feature or request styling About set and show rules or style properties meta labels Sep 4, 2023
@laurmaedje laurmaedje changed the title Element function to freeze state Ability to freeze state Nov 14, 2023
@laurmaedje laurmaedje added introspection Related to introspection. and removed meta labels Nov 24, 2023
@laurmaedje
Copy link
Member

I have posted some thoughts here: https://laurmaedje.github.io/posts/frozen-state/

@memeplex
Copy link

memeplex commented Aug 5, 2024

I've been reading the post and pondering on it. I would like to suggest an alternative angle to approach the issue. But at best I have a rough understanding of Typst architecture and internals, so what follows may be babbling.

If I understand correctly, the problem can be illustrated like this (Excalidraw permalink):

image

We have defined some content that depends on some state and we instance that content at different places in the document.

To produce its output each instance of the content must look at the sequence of updates of the state in document order, from the place where it's instantiated upwards.

But this turns ambiguous to which instance we are referring during some operations when we just say "content". Worst, there may be no instance at all. We want to fix this. So far so good.

Now a tentative solution has been expressed in terms of "frozen state":

  1. Somehow all instances of this content are actually the same instance (although instances of regular content aren't).
  2. More perplexing, parts of each instance may still change, like those depending on state2 in the diagram.

From this perspective, both are features of the content, and IMO this is where things get hand-wavy and a bit philosophical about different instances that are actually the same but not quite so.

Wouldn't it be more natural to express this at the instantiation point instead of at the definition point?

For example, regular #content instantiations would by default mean something like #content<here>, while you could explicitly say #content<place1> if you wanted content to be be instantiated with the state existing at place1, or #content<place1, (state2,)> if wanted the same but with state2 taken from place here instead.

Please note that this is not about the syntax (I don't care about it) nor about what places ultimately are (they may be simply labels, or perhaps contexts, I don't have enough knowledge to identify what's the closest underlying Typst concept, so I just called them places as well as I might have called them checkpoints).

@git001
Copy link

git001 commented Aug 5, 2024

@laurmaedje
Copy link
Member

laurmaedje commented Aug 5, 2024

@git001 Thanks for the heads-up. I made some changes the blog's build process. That broke things apparently. I'm rolling them back now. (edit: turns out it was because my GitHub student pro membership ran out and the repo was private ...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request introspection Related to introspection. styling About set and show rules or style properties
Projects
None yet
Development

No branches or pull requests

7 participants