How to implement a string buffer in Typst?

I am writing Typst code that needs to append a lot of strings onto each other. They are generated one by one, so - as someone with a long history in Java - I would like to write something like:

#let buf = string-buffer()
#buf.add("hello ")
#buf.add("world")
#buf.to-string()

It would be good if I could do this in linear time - so no #let buf = buf + str2, which would copy buf and therefore give me quadratic runtime.

I have two questions about this, which are of independent interest to me. First, is there already a mechanism in Typst that can do what I’m asking? For instance, is the += operator for strings constant time in the length of the left-hand side, so that a sequence of += operations would run in time linear in the total length of the concatenated string?

Second, how would I implement a string buffer “class” myself? I thought the slightly sneaky, pseudo object-oriented code below might work, but it fails on parts.push with “error: variables from outside the function are read-only and cannot be modified”.


#let string-buffer() = {
  let parts = ()

  let write(x) = parts.push(x)

  let to-string() = parts.join("")

  ("write": write, "to-string": to-string)
}


#let buf = string-buffer()
#{(buf.write)("hello ")}
#{(buf.write)("world")}
#{buf.to-string()}

Doing s1 += s2 is simple string concatenation, I guess the scripting engine just delegates to a Rust function so it should be fairly efficient (but I haven’t check). In any case I wouldn’t worry about it at an early stage of development.

Your string-buffer example can’t work in Typst because functions are pure (with some exceptions for built-in functions like array methods, but all user functions are truly pure). It’s not possible to write a function that has an effect other than returning a value. so (buf.write)("hello") cannot possibly modify anything. You could design an API around this limitation by having the user get a return value from buf.write that must be passed to the next call but it would be cumbersome.

But anyway your initial example actually works in regular Typst without any special class if you change the types and method names. Instead of

#let buf = string-buffer()
#buf.add("hello ")
#buf.add("world")
#buf.to-string()

you just write

#let buf = ()
#buf.push("hello ")
#buf.push("world")
#buf.join()
4 Likes