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
Typst will autodownload packages on import:
#import "@preview/typsy:0.1.0"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)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")) // falseMore 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.
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)
}),
)
}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.)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 ...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)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!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.
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.)
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.