What syntactic sugar do you most desire?

One thing that makes typst so much better than latex is that it makes easy things easy and in that spirit I was wondering what sorts of syntactic sugar do you most wish it had?

For me, I wish we had exponents in coding mode:


#{
a=2^5
//or
b=2**5
}

It would be also nice for the trig functions to be methods called on the number itself:


#{
56deg.cos

// vs

calc.cos(56deg)
}

Same with factorials and permutations:


#{
5.perm(4)

// vs

calc.perm(5,4)
}

Honestly, I wish there was more of a possibility to do method chaining vs function wrapping. There already is some there, but it makes me want more.

What are you wishes? Why are mine dumb? lol.

I’m interested in hearing some ideas.

3 Likes

It may seem an unexpected wish for a typesetting system, but sugar for monadic application. For example, suiji lets you create random numbers and you use it like this:

#import "@preview/suiji:0.4.0": *

#let random-point(rng) = {
  let x; let y
  (rng, x) = random-f(rng)
  (rng, y) = random-f(rng)
  (rng, (x, y))
}

#let rng = gen-rng-f(42)

#{
  let p; let x
  (rng, p) = random-point(rng)
  (rng, x) = random-f(rng)
  (p, x)
}

Note how each call to something using rng has to pass it in, store the returned changed rng, and process the actual return value. This comes from the immutability inherent to functional languages, but Monads are a solution for it.

Example in Haskell for the brave

Caution, very different syntax incoming:

randomPoint = do
  x <- randomF
  y <- randomF
  return (x, y)

rng = genRngF(42)

main = generate rng (do
  p <- randomPoint
  x <- randomF
  return (p, x))

(I hope that’s roughly correct…) The short of this is:

  • in a do block, it’s implied that we manipulate a monadic value (here an rng). The value itself doesn’t even show up.
  • with <- we modify the monadic value, while at the same time storing a result of the operation in a variable
  • return is a function, not a keyword, but it essentially terminates the sequence of modifications and specifies the result, which can in turn be extracted by <-
  • the do block as a whole wraps up the result, so randomPoint does not result in (x, y); it results in a monad that can operate on an rng to give (x, y)
  • at some point we therefore need to provide the actual rng and access the actual result; this is what the generate function here (left as an exercise to the reader) does.

There are probably lots of monad-like structures in the Typst ecosystem (and I imagine custom types could lead to even more of them), but Suiji’s rng is probably one of the most obvious ones. Another obvious one is in my package stack-pointer:

#execute({
  ...
  let (x, ..rest) = foo(); rest
  ...
})

I’m not actually sure what the sugar could look like; the two examples are also not exactly the same. Taking some inspiration from Rust’s ?, ! could indicate a “mutation”:

#let random-point(
  // this function has a "mutable" parameter
  rng!,
) = {
  let x; let y
  (rng, x) = random-f(rng)
  (rng, y) = random-f(rng)
  // when using the sugar, the array is unwrapped,
  // and the `!` value is assigned to the parameter variable
  (rng!, (x, y))
}

#let rng = gen-rng-f(42)

#{
  (
    // by suffixing the function call, we're opting to use the sugar
    random-point(rng)!,

    // note that we couldn't do
    random-point(get-random())!,
    // the mutable parameter needs to be assignable

    // the same is possible with regular functions as well,
    // so it's just sugar
    { let result; (rng, result) = random-point(rng); result },
  )
}
5 Likes

I wish there was a way to easily “declare” all symbols in equations as “variables” to Typst, so renaming is much easier.

Renaming variables is very complicated if you do not adhere to strict rules for su(b/p)scripting. You could do it in a minimal number of steps, but it’s much easier with integrated support.

Litteraly writing let xi = xi can make things become very messy, very quickly.

2 Likes

I wish we had proper vector and matrix types and could do computations with them as in

let a = vector(1, 2, 3)
let b = vector(4, 5, 6)
let c = a.dot(b) * b / norm(b)^2 

This is similar to @William’s first example (I find the other examples a bit weird and not an improvement for readability).

@SillyFreak fancy stuff, but I dread the idea of having to read and understand such code :slight_smile:

@quachpas can you give a concrete example? When does let xi = xi do something useful apart from a copy + change of scope?

6 Likes

I wasn’t very clear, but just imagine that you have a fairly large number of variables, each being used in different ways with sub- and supscripts.

If for some reason you are in the situation where you wish to change your “mathematical symbol”, e.g., from xi to zeta, then you have to do a search and replace in the editor.

In equations, symbols are all exported by default so you don’t have to prefix it with the module, but a symbol is only that, a symbol.

If you consider a mathematical variable to be a programming variable instead, then you might get some useful features, such as renaming, i.e., changing what the variable corresponds to.

Obviously, the problem isn’t trivial, because you are dealing with a good number of steps. That is, you start with

#let xi = math.xi
$xi$ // here, xi is #xi replaced by sym.xi
$#xi$ // #xi is the variable xi

In the case above, the variable xi supersedes sym.xi. If you want to change xi to math.zeta, then you change the variable assignment to math.zeta.

#let xi = math.zeta
$xi$ // sym.xi
$#xi$ // math.zeta

In that situation, you still end up with a confusing notation, where xi:=zeta instead. Preferably, then you would rename the variable xi to zeta.

#let zeta = math.zeta
$xi$ // sym.xi
$#zeta$ // math.zeta

There you go! you modified the xi math. var. to zeta, but then any xi who didn’t correctly call zeta is “forgotten” and has to be fixed up manually.

The obvious follow-up question is: why would you assign a variable in the first place, why don’t you just search and replace?

I often end up encountering symbol conflicts in my equations, where I already used a letter in a different context, or with a different subscript. Ideally, if everything is linear and you do not use the previous meaning after a certain point, then you can just re-use the same variable. If both meaning are used at the same time, then you’re in trouble!

#let k = $k$ // k param
$k=1,...,K$ // k is an index
$i=1,...,N-k$ // k is a parameter

you could get in trouble very quickly if you search & replace too quickly or modify your variable without anticipating symbol conflict.

Best practice here is to obviously not re-use the same symbol for different meanings, but sometimes you’re just out of letters :rofl:.

If you scope the meaning using variable assignments, then you are able to map 1-to-1 a meaning to a symbol. You also get renaming variables, changing the content, etc. out of the box.

#let kindex = $k$
#let kparam = $k$
$kindex=1,...,K$ // index k
$i=1,...,N-kparam // variable k

but frankly, this syntax is ugly! I wish there an easier way to map “meaning” to “symbol” and still get precious QoL features. You could abbreviate even further with ki and kp, but then its harder to know what is what. Using a dictionary is similar, but even worse because dictionary.get is not “inlined” in equations, so you have to add #k.index, and it’s still ugly.

I have no idea what the “ideal” syntax would look like, but it is probably not using programming variables as it reduces the advantage of using Typst in the first place (for me at least), which was to have a human-friendly equation syntax, similar to troff or lout.

1 Like

Thanks for the clarification.

I think the comments are wrong: in both cases $xi$ and $#xi$ are the same thing: they refer to the user variable xi (so in the second example you get twice the symbol zeta).

It seems to me that you want to

  • have the same identifier for different meanings/renderings,
  • have the possibility to change the rendering of this identifier in some uses without affecting other uses,
  • have things not confusing.

Aren’t these requirements inherently conflicting?

I must say I don’t find the following ugly at all:

#let kindex = $k$
#let kparam = $k$
$kindex=1,...,K$ // index k
$i=1,...,n-kparam$ // variable k

Straightforward code, clear and with meaningful names and you can easily change the rendering. I think the general idea of having an identifier that reads like a symbol but renders as something else is always going to be confusing, so using meaningful names as in this example is the way to go. Or do you know of another system that does this better?

I do think it’s probably the best I have ever had, but there are still nitpicks I guess! As for whether such a system is even possible in the first place, I have no idea.

In clearer words, I think what I would like is symbolic programming in Typst’s equations lol. I realize it’s a bit far-fetched. I think Mathematica (Wolfram Language) does it to a point where they even do static analysis on the code before running.

The random number stuff you pointed to would be so nice. I needed to use some about a month ago, and I would have appreciated it.

while I would argue that 56deg.cos is nicer in that you don’t have to remember to call calc first, I think the real payoff is when you are doing multiple operations. You read it across in the order that you do them.

say you wanted to reverse a number

in ruby it is 123456.to_s.chars.reverse.join(“”).to_i which I find extremely readable.

say it was implemented as each of those things wrapping it:

to_i( join( reverse( chars( to_s( 123456 ) ) ) ,“”) )

not only is the first far more readable, it is in the order you would type it so it makes writing it out smoother. In my opinion.

1 Like

This is just a small thing, but I’d like ---- to be a horizontal line by default. I think this follows markdown precedent, but I can imagine it’s maybe impossible to combine with the existing -- and --- shorthands without conflict. (I have also seen the existing show rule workaround that enables ----- to work.)

*** could also be a possible shorthand for a horizontal line or other horizontal separator or decoration. I think typst benefits from having many small built-in syntaxes like this, the more built in features typst has the richer templates can feel when they customize all these aspects.

Picture from: min-book package.

restructuredtext calls these transitions or dividers, which are a more implementation neutral term.

5 Likes

I noticed while typing in Obsidian the other day that there are several ways to obtain a divider: three dashes, three stars (on their own on a line, has a different meaning within text), and three underscores.

Maybe that triple underscore could be adopted by Typst, when on a line by itself?

1 Like

I wish some marks that can be used in parameter list in function definitions to mark positional parameters and named parameters. For example, in Python, parameters before / must be positional, and parameters after * must be named. But others can be either named or positional. By doing so, parameters having default values can be captured by their position.