Why do I receive an "Unexpected argument" error in anonymous function creater function?

I want to create my own counter with different levels. Sadly I’m struggling with updating the value. Now I just get the error message: “unexpected argument”

#let template(
  exercise-heading-prefix: "Exercise",
  exercise-heading-numbering-format: "1",
  subexercise-heading-prefix: "",
  subexercise-heading-numbering-format: "a)",
  subsubexercise-heading-prefix: "",
  subsubexercise-heading-numbering-format: "(i)",
) = {
  // custom exercise headings
  let counter-exercise = counter("counter-exercise")

  let counter_incr(i) = a => {
    // to array
    if type(a) == int { a = (a,) }
    assert(type(a) == array, message: "counter update got unexpected type")

    // a = a.slice(0, i)
    // a.at(-1) += 1

    while a.len() < 3 {
      a.push(0)
    }
    a.at(i - 1) += 1
    for idx in range(i, 3) {
      a.at(idx) = 0
    }

    return a
  }

  let base_exercise_heading(prefix, num-format, level, content) = {
    let w = ("medium", "regular", "regular").at(level - 1)
    let fontsize = 16pt - 2pt * (level - 1)
    let fup = counter_incr(level)
    context counter-exercise.update(fup)
    text(size: fontsize, weight: w)[
      #prefix
      #context {
        counter-exercise.display(num-format)
      }
      #if content != [] [ \- #content ]
    ]
  }

  let exercise(prefix: none, num-format: none, content) = {
    let level = 1
    if prefix == none { prefix = exercise-heading-prefix }
    if num-format == none { num-format = exercise-heading-numbering-format }
    base_exercise_heading(prefix, num-format, level, content)
  }

  let subexercise(prefix: none, num-format: none, content) = {
    let level = 2
    if prefix == none { prefix = subexercise-heading-prefix }
    if num-format == none { num-format = subexercise-heading-numbering-format }
    base_exercise_heading(prefix, num-format, level, content)
  }

  return (
    "exercise": exercise,
    "subexercise": subexercise,
  )
}

#let (exercise, subexercise) = template()

#set heading(numbering: "I.")

= Mandatory Exercises

#exercise[]

Here we go. #lorem(30)


#exercise[Binary]

#lorem(50)

#subexercise[]\
#lorem(10)

#subexercise[]\
#lorem(12)

#exercise[Hexa-decimal]

#lorem(30)


= Elective Compulsory Exercises


#exercise[Foo]

#lorem(50). That's all. Trivial.


#exercise(num-format: "I")[Roman]

That was easy.

#exercise[]

#lorem(20)

EDIT: sry missed to send the error, here is a picture

I don’t understand why it’s complaining about an unexpected argument but it is actually pointing to the parameter definition of this anonymous function.

Oh, I think I got it. the counter.update() function calls my anonymous function with the current counter value with each number a separate argument.

At this point I should probably write (..a) => ....

As a bonus: Is this an acceptable way to implement a custom multilevel counter? Or am I missing a much more convenient way?

This here is a working solution:

#{
  let counter-exercise = counter("counter-exercise")
  let counter_incr(i) = (..a) => {
    a = a.pos()
    while a.len() < 3 { a.push(0) }
    a.at(i - 1) += 1
    for idx in range(i, 3) { a.at(idx) = 0 }
    return a
  }
  let base_exercise_heading(prefix, num-format, level, content) = {
    let w = ("medium", "regular", "regular").at(level - 1)
    let fontsize = 16pt - 2pt * (level - 1)
    context {
      counter-exercise.update(counter_incr(level))
      let val = counter-exercise.get().at(level - 1) + 1
      text(size: fontsize, weight: w)[
        #prefix
        #numbering(num-format, val)
        #if content != [] [ \- #content ]
      ]
    }
  }
}

I’m fine with this for now…

However I still wonder:
Is that really the way to do it? Seems a bit ugly to me. I can well imagine that I have fabricated something here that is actually much simpler.

Would really appreciate your thoughts ;)

Hey @Xodarap, welcome to the forum! I’ve updated your post title in accordance with our question post guidelines: How to post in the Questions category

Make sure your title is a question you’d ask to a friend about Typst. :wink:

I believe you can replace counter-exercise.update(counter_incr(level)) by counter-exercise.step(level: level); see docs here: Counter Type – Typst Documentation

Thank you for your answer.

Yes, it seems that step with the level parameter behaves like that. Unfortunately it doesn’t state this behavior in the documentation. This is why i thought that complicated about this in the first place. There is definitely room for improvement.

There is still a problem: the step-update of only takes affect right after the surrounding context environment. One possible workaround would be to split the function into two separate context blocks. But is that the supposed design?

#{
  // custom exercise headings
  let counter-exercise = counter("counter-exercise")

  let base_exercise_heading(prefix, num-format, level, content) = {
    let w = if level == 1 { "medium" } else { "regular" }
    let fontsize = 16pt - 2pt * (level - 1)
    context counter-exercise.step(level: level )
    context text(size: fontsize, weight: w)[
      #prefix
      #numbering(num-format, counter-exercise.get().at(level - 1))
      #if content != [] [ \- #content ]
    ]
  }
}

This code does the trick. But there are still 2 context expressions.

You can remove the first context; .step() doesn’t require context. But yes, this is intended. The .step() function simply inserts into the document a command that says “from here onwards in the document, any attempt to read the counter will see its value increased by 1.” Now, what context does is: it creates an element which must be placed into the document. That element is responsible for running the code you wrote inside the context block using the document introspection information made available to it. In particular, when you write here() inside the context block, you will always get the same location: the location of that element. Also, when you get counter values by writing .get(), that is equivalent to .at(here()), so you will always be getting the counter value at the location of that element. Therefore, when you step the counter inside a context block, the updated counter value isn’t visible inside that same context block because you’re always using the location at the start of it. The only way to react to the updated value is to begin the context block after the step.