Exercise and solution together in source, rendered separately

I’d like to keep the typst source code for exercises and solutions in the same place. The exercises should be rendered in place. But the solutions should be collected and rendered in a section at the end, with links back to the exercises.

I’ve seen several exam-like packages. But I don’t think I’ve seen this feature.
My use case is a book-like (or monograph) document. I currently just do it manually, adding labels, skipping back and forth.

If I knew scripting better, I suppose I could put the content into an array with labels, and process it at the end. But I just started with typst.

Maybe you can do something like this:

#let show-solutions = true
#let all-problems = state("all-problems", ())

#let problem(title: "Example title", statement: [], solution: []) = {
  // Collect our title and solution into our all-problems state
  context {
    let current-problems = all-problems.get()
    current-problems.push((title: title, solution: solution, location: here()))
    all-problems.update(current-problems)
  }
  // Display the statement
  [
    == #title #label(title)
    #statement
  ]
}

= Problem list

#problem(
  title: "Basic arithmetic",
  statement: [
    1. 1+1 = ?
    2. 2+1 = ?
  ],
  solution: [
    1. 1+1 = 2
    2. 2+1 = 3
  ]
)

#problem(
  title: "More stuff",
  statement: [
    1. something
  ],
  solution: [
    1. some solution
  ]
)


#if show-solutions [
    #pagebreak()
    = Solutions
    #context {
      let problems = all-problems.get()
      for p in problems [
        == #p.title #link(p.location, text(blue)[(go to problem)]) 
        #p.solution
      ]
    }
]

Breakdown

  1. The show-solutions flag allows you to switch between rendering or not rendering the solutions.
  2. We have the #problem wrapper for our problems. This enables writing the problems and solutions in the same place in your source file.
  3. As variables defined outside our functions are read-only, we have to use state in order to keep track of our solutions and show them at the end. Notice that the get function for state is contextual, so we have to use context when using it.
  4. For each problem we store its title, solution and location so we can link back to it.

I assumed that you want to write a problem and its solution in the same place. If not, you can ignore all the state stuff and the problem wrapper and just “statically” write the solutions in the if block.

1 Like

You are using all-problems.get() and all-problems.update(current-problems) in the same context, which always(?) leads to the converge issue, provided this happens 5 times. And it does here, if there are 5+ problems:

#let show-solutions = true
#let all-problems = state("all-problems", ())

#let problem(title: "Example title", statement: [], solution: []) = context {
  let current-problems = all-problems.get()
  current-problems.push((title: title, solution: solution, location: here()))
  all-problems.update(current-problems)
  [
    == #title #label(repr(title))
    #statement
  ]
}

= Problem list

#for n in range(1, 6) {
  problem(title: [Problem #n])
}

#if show-solutions [
  #pagebreak()
  = Solutions
  #context {
    for p in all-problems.get() [
      == #p.title #link(p.location, text(blue)[(go to problem)])
      #p.solution
    ]
  }
]

To fix this, you should pass closure to the update function instead of overriding the value. Also the title can’t be content, because of the label, which is “fixed” with repr() content wrapper.

#let show-solutions = true
#let all-problems = state("all-problems", ())

#let problem(title: "Example title", statement: [], solution: []) = context {
  let problem = (title: title, solution: solution, location: here())
  all-problems.update(problems => problems + (problem,))
  [
    == #title #label(repr(title))
    #statement
  ]
}

= Problem list

#for n in range(1, 6) {
  problem(title: [Problem #n])
}

#if show-solutions [
  #pagebreak()
  = Solutions
  #context {
    for p in all-problems.get() [
      == #p.title #link(p.location, text(blue)[(go to problem)])
      #p.solution
    ]
  }
]