Function dynamic declaration

Hello everyone!

I have been trying to port a small LaTeX package that I’ve made based on coloredits. It is a package for annotation of documents by multiple authors. One of its features is the dynamic declaration of macros whose names are specified by one initial string (the author’s name). If I add an author named alvaro, the following macros are automatically created: alvaroadd, alvaroremove, alvaroreplace, alvaromark, alvarocomment, and alvaronote. While porting it to Typst, I have found no way to dynamic declare a function like this in order to simulate this behaviour.

This is the Typst code (in a summarized version):

#let default-color = blue

#let authors = state("authors", (:))
#authors.update(("default": (added: default-color, removed: default-color.transparentize(50%), background: default-color.transparentize(90%))))

#let add-author(name, color: default-color) = {
  authors.update(old => old + ((name): (added: color, removed: color.transparentize(50%), background: color.transparentize(90%))))
}

#let note(body, author: "default") = {
...
}

#let add(body, obs: none, author: "default") = {
...
}

#let remove(body, obs: none, author: "default") = {
...
}

#let replace(old-body, new-body, obs: none, author: "default") = {
...
}

#let mark(body, obs: none, author: "default") = {
...
}

#let comment(body, author: "default") = {
...
}

This is the current use for the package:

#add-author("alvaro", color: green)
#let alvaroadd = add.with(author: "alvaro")
#let alvaroremove = remove.with(author: "alvaro")
#let alvaroreplace = replace.with(author: "alvaro")
#let alvaromark = mark.with(author: "alvaro")
#let alvarocomment = comment.with(author: "alvaro")
#let alvaronote = note.with(author: "alvaro")

#alvaroadd(obs: [Observation.])[Add test.].

Currently, I have to explicitly declare each of the author’s functions using #let <authorname>add = add.with(author: "authorname") for each author. I would like to ask you if there is a way to automatically declare these functions? This is what I would like to happen:

// Functions "alvaroadd", "alvaroremove", etc. are automatically declared inside "add-author".
#add-author("alvaro", color: green)
#alvaroadd(obs: [Observation.])[Add test.].

Thank you very much. Cheers!

1 Like

Dynamically declaring variables in the way you want is not possible (except with the eval function’s scope parameter, but that’s not useful here). There are still methods to achieve an alright API for your case, but for the moment all would come with sacrifices. The feature that would improve this is dynamic modules (or custom types), but both require design work still and are probably a few releases away from shipping.

As a first option, it may just make sense to have the author be a positional string argument. Then \alvaroadd{body} would look like #add("alvaro")[body]. You could additionally make the author optional by using an argument sink and checking the positional arg length.

Secondly, you could just keep your current approach, but use array/dict destructuring to reduce the code size and noise.

#let (
  add: alvaroadd,
  remove: alvaroremove,
  ...,
) = author("alvaro", color: green)

Finally, you could have the author function from above still return a dictionary, but use the dict fields as functions directly. If we had dynamic modules, something like this would indeed be my ideal API. Unfortunately dictionary fields are still the best option we have for this case.

#let alvaro = author("alvaro", color: green)
// with dynamic modules:
#alvaro.add(param: 1)[body]
// with dict methods: 🙁
#(alvaro.add)(param: 1)[body]

A theme you’ll notice in these options is that Typst is much more clear about where code and values come from than latex. This really improves Typst’s understandability, but has tradeoffs and should impact your expectations on whether and how latex packages can be translated ergonomically.

5 Likes

Thank you very much, Ian! I will see if one of these options can improve the current implementation.

Cheers!

1 Like

I had another idea for how it could be done. This is just an exploration of another alternative. I don’t know if it’s actually better. But with nested function calls, we have a few other options for how to design the interface. For example like the following - two three versions!

First version: State

// state version
#let by-author(author, body, color: blue) = {
  state("author").update((author, color))
  body
  state("author").update(none)
}

#let alice = by-author.with("Alice", color: green)
#let bob = by-author.with("Bob", color: red)

#let note(body) = context {
  let info = state("author").get()
  let (author, color) = if info != none {
    info
  } else {
    panic("invalid: no author")
  }
  set text(fill: color)
  body
  footnote[by #author]
}

#alice(note[My note])
#bob(note[Another note])

Second version: Style context

// style context version
#let by-author(author, body, color: blue) = {
  // save this information for later
  show <__author-marks>: set bibliography(title: metadata((name: author, color: color)))
  body
}

#let alice = by-author.with("Alice", color: green)
#let bob = by-author.with("Bob", color: red)

#let note(body) = {
  [#context {
    let bt = bibliography.title
    // NOTE that if you do anything with bibliography or with elembic in an author scope, then you'll need to reset bibliography.title more properly, and that's possible, but not done here.
    set bibliography(title: auto)
    if type(bt) == content and bt.func() == metadata {
      let (name, color) = bt.value
      set text(fill: color)
      body
      footnote[by #name]
    } else {
      panic("invalid: no author")
    }
  }<__author-marks>]
}

#alice(note[My note])
#bob(note[Another note])

And other variants of the nested functions are possible - using query, or direct show/set rules.

The rendered output of the both examples is the same:

Third version: Simpler style version

This is slightly less wild than the previous, but using the same idea. Use labelled metadata to stand in for custom elements. You could also use any other thing - labelled text, figure, etc

// plain styled metadata for custom elements version
#let note(body) = [#metadata((note: body))<__author-note>]
#let by-author(author, body, color: blue) = {
  show metadata.where(label: note[].label): mt => {
    set text(fill: color)
    mt.value.note
    footnote[by #author]
  }
  body
}

#let alice = by-author.with("Alice", color: green)
#let bob = by-author.with("Bob", color: red)

#alice(note[My note])
#bob(note[Another note])
2 Likes

You can simplify this so that instead of add: alvaroadd you only write alvaroadd. The author function returns a dictionary, and you have control over the keys. So instead of

#let author(..args) = {
  (
    add: add.with(..args),
    ...
  )
}

You can write the intended variable name:

#let author(..args) = {
  let name = args.pos().first()
  (
    name + "add": add.with(..args),
    ...
  )
}
3 Likes