Typst is a typesetting system similar to LaTeX. It’s also a dynamically typed programming language with value semantics, so of course the natural question is: Can we implement the Haskell lazy infinite lists trick in Typst?

Typst doesn’t have a built-in lazy evaluation feature, but we can simulate thunks with lambdas. Basically, if we want to defer executing something, we’ll wrap it in a function that takes a dummy argument, and then call that function when we actually want to do the execution. The lists themselves will be represented as cons cells using Typst dictionaries. Here’s how to lazily map over a list:

#let map(f, l) = (
  head: f(l.head),
  tail: _ => map(f, (l.tail)(0)),
)

Now let’s define the positive integers as usual:

#let ints(_) = (head: 1, tail: _ => map(i => i + 1, ints(0)))

We can print out the first 10 integers using a loop (instead of a recursive function since Typst has a ridiculously low max recursion depth):

#let take(l, n) = {
  let ret = ()
  let cur = l(0)
  for i in range(n) {
    let ret = ret.push(cur.head)
    cur = (cur.tail)(0)
  }
  ret
}

// Prints (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
#take(ints, 10)

Next, we can implement integration:

#let zipWith(f, l1, l2) = (
  head: f(l1.head, l2.head),
  tail: _ => zipWith(f, (l1.tail)(0), (l2.tail)(0)),
)

#let integrate(l, c) = (
  head: c,
  tail: _ => zipWith((i, j) => i / j, l(0), ints(0)),
)

#let expSeries(_) = integrate(expSeries, 1)

#take(expSeries, 10)

#let evalAt(n, l, x) = take(l, n).rev().reduce((acc, a) => a + acc * x)

// Prints 7.38905609893065
#evalAt(50, expSeries, 2)

// Prints 7.38905609893065
#calc.exp(2)

And finally for the fun stuff! Typst doesn’t support mutual recursion, so we need a small workaround:

#let sine(cosine, _) = integrate(cosine, 0)

#let cosine(_) = map(x => -x, integrate(sine.with(cosine), -1))

#let sine = sine.with(cosine)

#take(sine, 10)

// Prints −0.4161468365471426
#evalAt(50, cosine, 2)

// Prints −0.4161468365471424
#calc.cos(2)

To test Typst’s performance, I tried #evalAt(2000, cosine, 2), which takes 7.5 seconds on my machine (although I first had to recompile Typst with an increased max recursion depth). Although Typst automatically memoizes function calls, it doesn’t seem to be effective here and the runtime ends up accidentally quadratic. Even my Python implementation of this trick is significantly faster.

I also tried the super cursed idea of using strings and eval() for lazy evaluation, and as expected, the performance is an order-of-magnitude worse.

#let map(f, l) = (
  head: f + "(float(" + l.head + "))",
  tail: "map(\"" + f + "\", " + l.tail + ")",
)

#let ints = (head: "1.0", tail: "map(\"(i => i + 1)\", ints)")

#let zipWith(f, l1, l2) = (
  head: f + "(float(" + l1.head + "), float(" + l2.head + "))",
  tail: "zipWith(\"" + f + "\", " + l1.tail + ", " + l2.tail + ")",
)

#let integrate(l, c) = (
  head: c,
  tail: "zipWith(\"((i, j) => i / j)\", " + l + ", ints)",
)

#let expSeries = integrate("expSeries", "1.0")

#let sine = integrate("cosine", "0.0")

#let cosine = map("(x => -x)", integrate("sine", "-1.0"))

#let evil(l) = eval(l, scope: (
  map: map,
  zipWith: zipWith,
  ints: ints,
  expSeries: expSeries,
  sine: sine,
  cosine: cosine,
))

#let take(l, n) = {
  let ret = ()
  let cur = l
  for i in range(n) {
    let ret = ret.push(evil(cur.head))
    cur = evil(cur.tail)
  }
  ret
}

#let evalAt(n, l, x) = take(l, n).rev().reduce((acc, a) => float(a) + acc * x)

// It works, but at what cost?
#evalAt(50, cosine, 2)

So yes, if you misuse a typesetting system badly enough, you can get lazy infinite lists out of it. Typst even generates a nice little PDF with the outputs of all these cursed functions.

I also implemented this trick in OCaml a few months ago but never got around to blogging about it, so I’ll just awkwardly leave a link to that code here I guess.