1. 67

Investigating the Typst programming language.

    1. 18

      After however many years of LaTeX, and eventually the wonder of LuaLaTeX (yes, \directlua), Typst feels like a gift from heaven.

      Typst the language makes so many things not only possible, but within easy reach. It’s got a few quirks, and debugging is still tricky. But overall, I really enjoy using it.

      Typst is capable enough to do most things most people need for most of their docs. There are a few gaps, like column balancing. But overall, I’m able to use it for real work.

      I don’t foresee needing LaTeX again in my life. Thank you Knuth and Lamport. It was extremely useful! But it’s time to move on.

      1. 11

        I’ve been wondering when there would be a good introductory blog post about Typst the language (and whether I might need to write it myself). Luckily, my laziness has paid off and the author has done a better job than I would’ve! This is a really lovely overview. I particularly appreciate that extra time is taken to explain each new concept, even if they might be conceptually simple. E.g. the implicit conversion of strings into content. These concepts become intuitive quickly, but they don’t start out that way, and as a heavy user I often forget to explain certain topics to newcomers when teaching.

        I bet that @laurmaedje will want to address most of the suggestions. However, I’ll chime-in that for the “tweaked” show rule, I would probably prefer this spelling:

        #show heading.body: it => [✿#it;✿]
        

        One of my favorite parts of the language that the post doesn’t mention is the way that code-blocks automatically join values, and how this gives Typst some of the complex features of other programming languages almost for free.

        To explain, here are three ways to write the following content in Typst:

        Some examples of greetings:

        • hi
        • hello
        • how are you
        1. Using Typst markup:

          Some examples of greetings:
          - hi
          - hello
          - how are you
          
        2. Manually joining content values with a plus sign:

          #{
            [Some examples of greetings:] + [- hi] + [- hello] + [- how are you]
          }
          
        3. Using a block with a for-loop to join the content implicitly:

          #{
            [Some examples of greetings:]
            for greeting in ([hi], [hello], [how are you]) {
              [- #greeting]
            }
          }
          

        The last example likely doesn’t feel that surprising, but from a programming language standpoint it’s actually incredibly rare. For example, in Rust only the last value of a block is returned and semicolons are required to ignore non-final values. In Typst all values in a block are returned because they’re joined together.

        The truly exciting part of this implicit joining comes when we find out that “joining” is overloaded for arrays and dictionaries.

        We can join arrays using a plus statement:

        #{
          (1,) + (2,) + (3,) == (1, 2, 3)
        }
        

        Or we can do the joining implicitly by using blocks in a for-loop!

        #{
          for i in (1, 2, 3) { (i,) } == (1, 2, 3)
        }
        

        If you’re familiar with it, this should remind you of the following list-comprehension syntax in Python:

        [i for i in [1, 2, 3]] == [1, 2, 3]
        

        In Typst we get this same language feature for free! And it’s more elegant because the loops are composable with the rest of the language since they can contain arbitrary statements inside.

        1. 4

          #show heading.body: it => [✿#it;✿]

          Oh yes, that’s definitely better spelling than heading > body. The heading > body syntax was only meant to be illustrative, @laurmaedje introduced it to me as “Ignoring syntax for the moment and just writing it CSS-like”, but I forgot to include that context in the post.

          One of my favorite parts of the language that the post doesn’t mention is the way that code-blocks automatically join values

          I do mention it briefly—see “Joining Content”—though I hadn’t realized you could join things other than content (hence “Joining Content”). It is indeed a really interesting part of the language!

          The place to look for this sort of thing in other languages is in macro systems and the equivalent. You can do something similar in Zig with inline for, and in Racket with quasiquotation splicing (https://docs.racket-lang.org/reference/quasiquote.html). Other languages too, I’m sure, though I can’t think of any off the top of my head.

          Nice observation that you can effectively get list comprehensions by implicitly joining arrays.

          What’s the set of things that can be joined together? I’ve now seen content, arrays, and maps, which would suggest that you can join everything you can combine with +, except that IIRC you can’t join numbers.

          1. 3

            What’s the set of things that can be joined together?

            From a quick test I have: content, arrays, dictionaries, strings, and byte-strings. Also I think the none value can join with anything, but is just ignored (let-statements technically evaluate to none). I remember a discord comment speculating that if this principle of joining were embraced, it would be fun if you could join function values and get function composition, although that might be going a bit too far.

            I’ve definitely felt the similarity to macro systems! It feels almost like Typst code is a macro for generating document content, it feels very Lisp-like (maybe the right concept is “rhyming”?). It’s funny how Typst is made to supersede the macro-expansion-language of Latex. Maybe if Latex was designed as a Lisp…

          2. 3

            The for loops remind me a bit of this thread:

            https://old.reddit.com/r/ProgrammingLanguages/comments/1fyxu4n/whats_the_coolest_minor_feature_in_your_language/lqxsoms/

            In Dart, you can put the for loops in the middle of a collection element:

            var args = [
              if (debug) '--debug',
              for (var option in options) 
                '--$option',
              ...paths,
            ];
            

            In YSH, you can also build up a data structure with a for loop: https://www.oilshell.org/release/latest/doc/hay.html#conditionals

            Rule foo.o {   # node
              inputs = []  # populate with all .cc files except one
            
              for name_ in *.cc {
                if name_ !== 'skipped.cc' {
                  call inputs->append(name_)
                }
              }
            }
            
            # evaluates to something like
            
            { type: "Rule"
              inputs: ["foo.cc", "bar.cc"]
            }
            

            I think the explicit append is OK, but now I remember I had this idea for a - prefix operator to build up a list, sort of like YAML and Markdown. It could be:

            List (&inputs) {
              - 'before'
              for name in *.cc {
                - "prefix/$name"
              }
              - {after: 42, my: 'dict'}
            }
            
            # evaluates to
            
            ["before", "foo.cc", "bar.cc", {after: 42, my: 'dict'}]
            

            That feels a bit more magic, but maybe it’s understandable?

            1. 4

              This is reminding me of Swift result builders.

              1. 3

                Wow yes, I didn’t know Swift had this!

                @ComplexStringBuilder func countDown() -> String {
                    for i in (0...10).reversed() {
                        "\(i)…"
                    }
                
                    "Lift off!"
                }
                
          3. 5

            Nice post! I have been playing with Typst a bit and, as a fellow programming-language person, I was thinking about the same lines – it would be interesting to figure out which domain-specific features emerge as important in Typst’s design.

            Currently my impressions are as follows. Note that I am not very knowledgeable about typst yet, so it is very possible that some of them are wrong.

            • Typst is designed in a way that should allow incremental rendering. You discuss this in your post as “it is easy to reason about the fact that … will not modify the settings of …”, but one can also see it as incrementality-preservation.
            • set/show are important and interesting, you covered that well in your post.
            • context and state are important and interesting – you mention them as missing. If I wanted to write a formal semantics of Typst, I think they would be a point of focus. Currently my impression is that there is a built-in state effect, with a static discipline to be able to reason about which parts of the result do depend on the state.
            • I am disappointed with how hard it is to tweak Typst formatting choices, from the perspective of importing an existing style and then tweaking its content. In my experiments I have found that this works very poorly, and I need to copy-paste the source to modify it instead. My impressions are that LaTeX or Emacs do a much better job at it, that maybe Typst could do better with some programming conventions that would preserve this flexibility (for example: put stylable decisions in mutable variables, instead of hardcoding them), and that it might still need new language features around aspect-oriented programming (Emacs’ defadvice.) Some of your comments on #set and #show go along these lines as well.

            (This feels like something possibly worth thinking about seriously – professionally. Maybe it would be a cool internship for someone to define a Core Typst language, elaboration rules from a representation of the current syntax, and an operational semantics and type system.)

            1. 3

              Note: as a Typst newcomer, one thing I don’t like is the documentation. Many things that clearly should be explained there are clearly missing, and I suspect the maintainers are not doing a good enough job ensuring it remains up-to-date and complete – without strong social conventions around updating and improving documentations, thing can fall down in quality quickly. (For example, when you are in “math mode” context, how do you go back in “text mode”? Obviously that should be explained clearly in the “Syntax” documentation page.)

            2. 1

              Minor, minor point: in your “common data types” table, we now have a standard “growable array” type in OCaml called Dynarray. (It was first released in the standard library in OCaml 5.2, which is the last release from May 2024.)

              1. 1

                Thanks, updated in the post.

              2. 1

                This article made me wonder how it would work as a language for Advent of Code. So I did the first day, its a bit slow since to does a lot of filtering of a array, but that can be optimized. https://gist.github.com/Erk-/13455a68639923fece919302319db299