Classes/structs, pattern matching, safe counters... and more!
Your one-stop library for programming tools not already in core Typst.
Pronounced ‘tipsy’, because I think that’s funny and it’s a nice pun on ‘Typst’. 😄
Provides tools for programming geeks:
- classes (i.e. structs, custom types)
- pattern matching
- enums
- safe counters (no need to choose a unique string)
- trees-of-counters (i.e. subcounters)
- string formatting
- namespaces of objects that can be mutually referential
- runtime type checking
Installation
Typst will autodownload packages on import:
#import "@preview/typsy:0.1.0"
What’s in the box?
Classes
Classes with fields and methods:
#import "@preview/typsy:0.1.0": class
#let Adder = class(
fields: (x: int),
methods: (
add: (self, y) => {self.x + y}
)
)
#let add_three = (Adder.new)(x: 3)
#let five = (add_three.add)(2)
Pattern-matching
Simple type checking:
#import "@preview/typsy:0.1.0": Array, Int, matches
// Fixed-length case.
#matches(Array(Int, Int), (3, 4)) // true
// Variable-length case.
#matches(Array(..Int), (3, 4, 5, "not an int")) // false
More complicated match-case statements:
#import "@preview/typsy:0.1.0": Arguments, Int, Str, case, match, matches
// Option 1: if/else
#let fn-with-multiple-signatures(..args) = {
if matches(Arguments(Int), args) {
// ...
} else if matches(Arguments(Str), args) {
// ...
} else if matches(Arguments(Str, level: Int), args) {
// ...
} else {
panic
}
}
// Option 2: match/case
#let fn-with-multiple-signatures(..args) = {
match(args,
case(Arguments(Int), ()=>{
// ...
}),
case(Arguments(Str), ()=>{
// ...
}),
case(Arguments(Str, level: Int), ()=>{
// ...
}),
)
}
Observe the capitalisation. All patterns are capitalised to distinguish them from their corresponding type.
Enums
Also using the same pattern-matching capabilities as above:
#import "@preview/typsy:0.1.0": case, class, enumeration, match
#let Shape = enumeration(
Rectangle: class(fields: (height: int, width: int)),
Circle: class(fields: (radius: int)),
)
#let area(x) = {
match(x,
case(Shape.Rectangle, ()=>{
x.height * x.width
}),
case(Shape.Circle, ()=>{
calc.pi * calc.pow(x.radius, 2)
}),
)
}
Safe counters
Counters without needing to cross your fingers and hope that you’re using a unique string each time:
#import "@preview/typsy:0.1.0": safe-counter
#let my-counter1 = safe-counter(()=>{})
#let my-counter2 = safe-counter(()=>{})
// ...these are different counters!
// (All anonymous functions have different identities to the compiler.)
Tree counters / subcounters
Create trees of counters, including using existing counters as starting points. This is particularly useful for creating theorem counters that increment with the heading.
#import "@preview/typsy:0.1.0": tree-counter
// Set up counters
#let heading-counter = tree-counter(heading, level: 1)
#let theorem-counter = (heading-counter.subcounter)(()=>{}) // uses safe-counter internally!
#let corollary-counter = (theorem-counter.subcounter)(()=>{})
// Usage
#set heading(numbering: "1")
#let theorem(doc) = [Theorem #(theorem-counter.take)(): #doc]
#let corollary(doc) = [Corollary #(corollary-counter.take)(): #doc]
= First Section
#theorem[Let ...] // Theorem 1.1: Let ...
#theorem[Let ...] // Theorem 1.2: Let ...
#corollary[Let ...] // Corollary 1.2.1: Let ...
= Second Section
#theorem[Let ...] // Theorem 2.1: Let ...
String formatting
Rust-like string formatting:
#import "@preview/typsy:0.1.0": fmt, panic-fmt
#let msg = fmt("Invalid input `{}`, expected `{}`.", foo, bar)
// shorthand for `panic(fmt(...))`
#panic-fmt("Invalid input `{}`, expected `{}`.", foo, bar)
Runtime type checking
Wrap functions to check their inputs and outputs. This builds on top of the pattern-matching capablities above.
#import "@preview/typsy:0.1.0": Arguments, typecheck
#let add_integers = typecheck(Arguments(Int, Int), Int, (x, y) => x + y)
#let five = add_integers(2, 3) // ok
#add_integers("hello ", "world") // panic!
Namespaces of mutually-referential objects
Build a namespace by providing lambda functions which return their object. Access any object in a namespace via ns(object-name)
:
#import "@preview/typsy:0.1.0": namespace
#let ns = namespace(
foo: ns => {
let foo(x) = if x == 0 {"FOO"} else {ns("bar")(x - 1)}
foo
},
bar: ns => {
let bar(x) = if x == 0 {"BAR"} else {ns("foo")(x - 1)}
bar
},
)
#let foo = ns("foo")
#assert.eq(foo(3), "BAR")
#assert.eq(foo(4), "FOO")
For example, this can be used to implement mutually-recursive functions.
Documentation
All objects have detailed docstrings indicating their usage; see those for details.
The examples above demonstrate nearly every object in the public API. In addition to those above, the list of patterns that can be used for pattern-matching are:
Any, Arguments, Array, Bool, Bytes, Class, Content, Counter, Datetime, Decimal,
Dictionary, Duration, Float, Function, Int, Label, Literal, Location, Module,
Named, Never, None, Pos, Ratio, Refine, Regex, Selector, State, Str, Symbol,
Type, Union, Version
(They are capitalised to distinguish them from the underlying type.)
FAQ
Similar libraries:
- elembic offers a very different way to create custom classes.
- valkyrie offers object parsing that is somewhat similar to our type matching.
- headcount and rich-counters also offer tree counters. (Though I find our approach a bit simpler, and safer due to our
()=>{}
-using safe counters.) - oxifmt offers Rust-like string formatting. Theirs is far more developed and better than what we have; I just like avoiding dependencies.