I think one example is that both sin(x) and sin x are valid notation and common to use, and so a function doesn’t seem to be quite right, at least not if we want to allow the markup to mimic the math notation closely.
I see. But then my next question: Why is op not a Typst function, while math.abs is? (And if it is, why isn’t sin
then, which is a special case of op?
With other words, why don’t we implement op as a Typst function and circumvent the problems with that?
op
is a Typst function that you can use to make operators like sin
:
#let mysin = math.op("sin")
$ mysin(x) $
Here mysin(x)
is not a function call, it’s the value mysin
followed by (x)
. But the value mysin
was obtained by calling op
. You could also write
$ op("sin")(x) $
and get the same result.
You can already partially do this by sacrificing one of the symbol accents that works as a function already:
#let sin = sym.circle
#show sin: math.sin
#let accent-char = sin([]).accent
#show math.accent.where(accent: accent-char): it => {
math.sin + $(it.base)$
}
These now render the same,
but the first line is an actual function call.
$ sin(x) \
sin (x)
$
I would also vote for option B for consistency with other similar cases such as x^-1
which is rendered as x^(-) 1
. See also the related issue: Raising to the power of a negative number should not require parenthesis · Issue #5722 · typst/typst · GitHub
Thanks @Y.D.X for the visual summary.
Just to clarify as the case with f_i (x)
shows no representation on the “Next version” side: I guess this pattern will still be supported by the next version and render the same?
this pattern will still be supported
Yes; I omit it because I think f_i (x)
will become rare in the next version. Just likef (x)
today, which I believe most people would write f(x)
instead.
Just a data point. I was also hit by this today as I was translating my brother’s thesis from LaTeX to Typst.
$
"CF"(X, k) &= limits(sum)_(x in X) I_k(x) & quad "Class frequency" \
$
I would thus also be in support for option B.
I’m not a mathematician so I could be wrong, but I support option C or D for the following reasons:
-
Symbols of functions, like f, can be standalone. I mean, when we say there is a function, we don’t have to say f(x). In this case, f is math symbol. And f(x) is a kind of operation to map x to something else via function f. In most cases, attachments are symbols, not operations. Because of this, when we write
$f_i(x)$
, we would not expecti(x)
to be something attached, unlessi
is a user-defined “programming” function (stated as below). In this case, treating thei(x)
part as the attachment seems to be strange. -
Some special functions, like “sin”, they are predefined symbols, although it could be rare to see “sin”, “cos”, etc., alone.
-
However,
abs(x)
represents “|x|” in math. Thus, “abs” itself is not a symbol; instead, “abs(x)” the whole thing is. Becauseabs
is a programming function, not a mathematical symbol or operation. A standaloneabs
does not make mathematical sense. -
For other user-defined functions, I think they would probably prefer keep the whole thing together. As an example, I defined a function to show conditional expectation, like
#let Exp(x, ..args) = $E lr((#x mid(|) #args.pos().join(",")))$
When I write
$e^Exp(x,u)$
, I definitely want the part like $E(x|u)$ to show at the corner, instead of a “E” at the corner but “(x|u)” not.Another example is the Imaginary number. If we define a function
#let Im(a,b) = $#a + #b i$
then I also expect
$e^Im(1,2)$
to show something like “e^(1+2i)”, not “e^1 + 2i”.
Overall, I think it would be better to act differently for different components. If the attached component is a “programming” function, like abs
and user-defined functions, treat the whole thing as the attachment. For math.op
and normal math symbols, don’t show them like function calls.
I don’t know if it makes a difference for your argument, but from $e^Exp(x,u)$
you’d get “e^Exp” followed by “(x, u)”, not “e^E” followed by “(x|u)”; for $e^Im(1,2)$
you’d get “e^Im” followed by “(1,2)”, not “e^1” followed by “+ 2i”.
In other words, the issue isn’t that the function is evaluated and only part of the result becomes the superscript; it’s that option B would not call the function, and put the parameters outside the superscript.
I know I’m a bit late, but I think that an Option F is missing from the suggested solutions: Always greedily match subscripts as far right as possible. That is, f_i(x)
, f_1(2+2)
or g_3(y)
would put everything to the right of _
into the subscript. This eliminates all ambiguity, and lets the user determine this with spaces.
This behavior mirrors what new users encounter with implicit multiplication: Writing ab + x
produces an error, as ab
is parsed as a function. Thus, they have to write a b + x
. In exchange, they get functions without leading delimiters (#
or \
), leading to much cleaner syntax.
The same can be applied for subscripts. In long expressions, reducing parentheses makes code more readable. Letting users control subscripts with x_2(i+1)
or f_2 (i+1)
achieves this.
Cons:
- Requires more spaces in the simple cases such as
f_1 (x)
- For simple subscripts, this contradicts the expected behavior from LaTeX, unlike the proposed solution.
- Is a breaking change, (with wide-spread effect)
Pros:
- Achieves the same unambiguous parsing as the proposed solution
- Allows using spaces as a typographic tool, leading to more readable syntax-code than wide parentheses.
I hope this option can be considered – I think a minimal syntax is a key strength of Typst, and is a goal that should be pursued.
I’m a bit late, but still wanted to add a voice in favor of option F for the reasons presented in above comments. Editing and reading sources with many parentheses is more annoying imo than if we can use spaces (see #6442 (comment)).
Additionally, I would be in favor of making any Unary or Vary operator cause the immediately following element to be scripted as well, so that $e^-x$
would work as expected (and we can still write $e^- x$
if we only want the minus sign to be in the exponent): Raising to the power of a negative number should not require parenthesis · Issue #5722 · typst/typst · GitHub
Some time has passed since I’ve asked for feedback and there were quite a few responses on the PR and here in the Forum. There were some voices for most of the Options (stay as-is, revert to 0.3, runtime-dependant parsing), including for an option not included in my blog post: Basically Option A on steroids, where we expand the current behaviour to numbers (Option F).
If you’re interested in the individual arguments, I invite you to reread the discussion as I can’t fully reproduce everything here. But as a quick summary: The main argument in favor of Option B was predictability while the main argument in favor of Option A and F was terseness, primarily avoidance of additional parentheses, which can make equations harder to read.
There is no way to make everyone happy here, so it’s going to be a difficult decision either way. Personally, I believe that predictable, consistent, and intuitive syntax is more important than a bit of extra tersity (this is also in line with Typst’s generally design philosophy). While I wouldn’t say there is clear consensus, the majority of upvotes on both GitHub and in the Forum seem to share this sentiment. For this reason, I have decided to move forward with Option B.
Thanks to everybody who left feedback, in particular also those that argued for other options.
Has it also been considered to make x^-1
parse as x^(-1)
?
This is something I stumble over again and again, and I feel like there are few cases where people would like to write x^- 1
. But of course it would make the system more inconsistent
Personally, I think making it more inconsistent again for a small gain in brevity would be a step back. That’s how we got ourselves into the whole situation being discussed here in the first place.
Hello, I vote for the option C: Runtime parsing. I think that would preserve the math function behaviour and the parse act as expected. Although I’ve seen that the pull request in GitHub has been merged for option B, I want to share my criterion. I think option C would adapt better for the Typst’s general philosophy and would preserve its intelligence that makes it so awesome.
I would like to share my opinions as an end user. A majorty of Typst users would be starting to use Typst after the syntax change in 0.3 (unsure), so when learning to write math in Typst, they would learn that identifiers like f
or pi
or abs
tend to stick to parentheses, and will then learn to insert spaces as in a_f (x)
if they doesn’t mean a function call, and simply write a_f(x)
or f(x)/g(x)
if they want a function call, just like we’ve learned to write a_(i j k)
instead of a_ijk
. If we switch back to option A, then the behaviour of super/subscripts and fractions will be different, and it requires time for re-learning and getting used to the new behaviour. This new behaviour might be more intuitive for new users, but for older users this is a breaking change. Inserting a space between f
and (x)
is not that unintuitive after all. Perfectly intuitive math syntax would likely be impossible due to the vastness of math notations; imagine someone typesetting the derivative of f_1
, and they have to write f'_1
instead of f_1'
. Syntax is not about being intuitive, but about being easy to learn and consistent (and consistent between different versions!)
In any case, I would prefer to stick to the current behaviour, to reduce the number of breaking changes.
Edit: I would like to add that the benefit of Option A is not only terseness, but also, it’s cleaner. I personally think separating things using spaces rather than parentheses is more readable and aesthetically pleasing. Take this as my personal opinion though.
I’ve written a blog post arguing for option C on my website: Runtime Math Parsing Is the Solution Typst Deserves. I welcome discussion and comments here.
I’ve also implemented runtime math parsing as Pull Request 7003
@ian I have read through your post a few times now, and also read through mine again, and collected my thoughts.
With most of your points, I think they do have something to it, but I also want to push back a little on them. Let me unpack my thoughts:
Option B hides typesetting affordances
It is true that there are still differences between mathematical functions and Typst functions, primarily in how you can interact with them. That said, I have problems with the framing that option B is harmful in this regard. The fact that mathematical functions and typesetting functions work different in some regards does not make it worse for them to also work the same in other regards, or else we’d have to try to make them as different as possible.
Even if the two types of functions still work differently in some ways, I think
the predictability of how they interact with attachments is good to have.
Option B breaks trust in composability
With this argument, I’m conflicted. I see your point, but at the same time I also don’t really buy it. It essentially implies that just because function calls have highest precedence in most contexts, they must have maximum precedence everywhere. I think the same argument could be made for attachments: Why would an attachment compose differently with a function than with a symbol? To an extent, that also breaks trust in how a Typst attachment composes with other syntax. As a user, I could be left wondering why #let ff(x) = $bb(f)(#x)$
composes differently with my attachments than #let ff = $bb(f)$
.
I guess the reason I’m conflicted is the following: Yes, at the surface option B does break composability a bit, but so does option C, just in another way. If you go a bit deeper, neither does break composability particularly badly because in both cases you can compose, you just have to know the rules of composition. But for option B, the rule is much more obvious! The rule to add parentheses and when you need them is a simple one; it’s a rule that you can understand as a beginner without learning about what runtime and types even are.
Meanwhile, the rule of how runtime-parsed attachments compose is much harder to understand, as it requires context that is not only syntactically invisible to the compiler, but also obtuse to many beginners.
Laurenz also makes an argument about syntax highlighting for math under option B that I have some concerns with
This breaks down when you have false positives, or expressions with the identifier(…) syntax that aren’t functions, such as implied multiplication.
While I agree that syntax highlighting unfortunately doesn’t tell the full story about an equation with option B either, I want to clarify that the part of my argument that you quoted was exclusive about precedence, i.e. the fact that you can understand precedence purely based on syntax highlighting. You still cannot fully understand the mathemtical role a piece of syntax plays purely based on highlighting. Semantic highlighting would improve that (though also to an extent; for some things we just don’t have enough information generally).
However, I would argue that semantic highlighting and runtime parsing are separate concerns. (1) Semantic highlighting can improve highlighting in some scenarios regardless of option B or C. (2) Semantic highlighting can fix the issue that purely syntactical highlighting does not let you reason about precedence in option C. But that doesn’t change the fact that runtime parsing introduces the issue in the first place, while option B does not.
There are also a lot of places where runtime highlighting is not available or would complicate highlighting a lot (e.g. if we want semantic highlighting in docs or a blog post, that would now need full-blown compilation instead of just typst_syntax
). For purely syntactical highlighting cases, option C is strictly worse. It’s perhaps not the end of the world, but basic non-semantic highlighting would just be a bit less useful with runtime parsing, so there is a small downside here.
I think the point of highlighting Typst math isn’t to make the meaning of the mathematics clearer. The point of syntax highlighting is to make the meaning of the Typst source code clearer.
Precisely! I only wanted to argue that it makes precedence clearer (which is a Typst concern, not a mathematical one).
I don’t think there’s any reasonable subset of Typst’s math syntax that wouldn’t either require the use of a substantial amount of the Typst compiler or would be something that isn’t actually Typst.
I tend to agree that this is a larger problem with repurposing Typst’s math syntax outside of Typst than the runtime stuff. So I’m fine with leaving that argument off the table and considering only what’s best for Typst.
I think this is a fair point, but I disagree with it because I consider Typst’s normal context of use. The main way that most people write Typst is with a live, incremental update of the document in a different pane of their editor.
While I think there is a fair argument here, I do think it misses one important point. A crucial question, in my view, is why something is more readable in the first place. I’d argue that it’s typically the case because it’s more intuitive or easier to build an understanding for. Of course, I can check how it renders easily, but if I check every time without building a deeper understanding, I will never be able to write it correctly in the first place. And, on top, even if I have understood how it works, when using a custom definition, I will need to know how it was defined to know a thing as basic as what the precedence of it is.
I think that’s the main benefit of optimizing for readability rather than how many letters per second you can actually read as a power user. That’s why I don’t really buy into the “optimize for writability over readability” argument.
As a more structured response, let me recap my point on each of yours:
- Two harms of option B for new users
- Option B hides typesetting affordances: I agree that the distinction between math and Typst functions is still relevant, but I disagree that Option B makes this worse.
- Option B breaks trust in composability: I agree that it hurts composability of functions a bit (though I think “breaks trust” is a bit overly strong of a statement). My counterpoint is though that option C composes less predictably which is also important.
- What is Syntax Highlighting For: I agree that option B isn’t the holy grail syntax highlighting wise, but I would say it is strictly better than option C, even if not by much.
- The Downsides of Runtime Math Parsing
- Runtime math parsing doesn’t really change the status quo: Accepted
- Human Readability vs. Human Writability: I disagree because the core issue is not literally “reading”, but building an understanding.
I see that runtime parsing has upsides. It gives the good attachment behaviour for symbols, while avoiding additional parentheses in the function case. To an extent, I also agree with the reasoning that functions feel a bit more composable.
But the big concern I still feel is about predictability. Not only would it be hard for a beginner to build a mental model for the precedence of attachments (as they first need to understand the different kinds of data types at play); even as a seasoned user, you cannot predict the precedence of an attachment by just looking at the source. While you argue that instant preview alleviates this problem, for me the syntactical structure of an equation is such a basic concern that it needs to be clear just from the markup. (Perhaps, this is just a matter of philosophy.) On top, I fear that regardless of other differences between Typst and math functions the behaviour of the middle ground between symbols and functions (e.g. sin
) will remain unintuitive even for advanced users.
There is also the point about syntax highlighting and of course also the complexity that runtime parsing introduces. But compared to my concerns with predictability, these are honestly minor.
In the end, I remain unconvinced that saving these extra parentheses is worth making precedence so much more intricate to reason about. I also think that we’ve held back on a change here for about as long as possible. Really, this should have been reverted a long time ago as the status quo is so clearly worse than either option B or C.
It is clear that you are very passionate about this topic and have put a ton of effort into the post and especially also the PR (which looks very well done), but in the end I still believe that option B is the better choice for Typst. I hope I’ve made understandable why even if you disagree.
I realize this might be a bit late, but I’d like to share a few thoughts on precedence in Typst.
I think the current precedence rules in Typst are quite good. LaTeX’s precedence, especially regarding subscripts and superscripts, has always felt unintuitive to me since it does not align with mathematical precedence.
For Typst, I suggest that function application should have the same precedence as implicit multiplication, and higher precedence than ^
and _
.
For example, when implicit multiplication is considered, we have
a^2b = a^(2 b)
. This aligns with standard mathematical and programming precedence (see e.g. Mathematica, Maple, Julia, Maxima, etc).
In Typst, space plays a key role so a^2b
(implicit mult.) should differ from a^2 b
(no implicit mult.).
Here’s how this would map in practice:
$
proposed -> "kind" -> current interpretation
f_1(x) -> "implicit multiplication" -> f_( 1(x) )
f_i(x) -> "i(x) function or implicit mult." -> f_i(x)
f_i (x) -> "function f_i" -> f_i (x )
f^pi(x) -> "pi(x) function or implicit mult." -> f^pi(x)
f^pi (x) -> "function f_pi" -> f^pi (x)
e^abs(x) -> "function abs" -> e^abs(x)
$
This is almost identical to the current syntax, but fixing the inconsistent behavior between f_1(x)
and f_i(x)
, as pointed out by @Y.D.X, by considering f_1(x)
as a case of implicit multiplication. It also has the advantage of being nearly 100% backward-compatible.