EpicEricEE / typst-quick-maths

A Typst package for creating custom shorthands for math equations.
MIT License
8 stars 0 forks source link

quick-maths questions #1

Closed lumi-a closed 2 days ago

lumi-a commented 9 months ago

Background: I've been using your implementation of quick-maths to write a package handling Unicode super-/subscripts in maths-mode (thanks for the MIT license!), and have some questions about your implementation of quick-maths.

Possibly redundant recursion

Quick-maths applies convert-sequence to all sequences within a math.equation via this code:

  show math.equation: eq => {
    show sequence: seq => {
      let new = convert-sequence(seq, shorthands)
      if new != seq { new } else { seq }
    }

    eq
  }

This also works for sequences within sequences. However, in convert-sequence, you additionally apply convert-sequence to the children of the sequence itself as well, which seems redundant:

  let children = seq.children.map(c => convert-sequence(c, shorthands))

I tested the package after replacing this line with let children = seq.children instead, and it still seems to work as expected. Please correct me if I'm wrong, but mapping convert-sequence over the children doesn't seem to change them anyway, because children seem to never be sequences themselves. This would also explain why, in my tests of your current implementation, the run-time doesn't depend exponentially on the nesting-level of a sequence.

Content that is not a sequence

The sequence type is declared as #let sequence = $a b$.body.func(), which seems to be an array of content. However, some users might want to replace single pieces of content, which is not possible, as those are stored without being wrapped in an array. For example:

#import "@preview/quick-maths:0.1.0": shorthands
#show: shorthands.with(($+$, sym.plus.circle))
$a + b$   // Renders a⊕b (as expected)
$+$       // Renders +   (unexpected)
$a^+$     // Renders a⁺  (unexpected)
$sqrt(+)$ // Renders √+  (unexpected)

In the unexpected cases, the issue is that the content [+] is not part of a sequence. In the expected first example, the content [+] is part of the sequence ([a], [ ], [+], [ ], [b]).

I don't know how this could easily be solved, as we can't expect an upstream-change wrapping even single pieces of content into a sequence.

Perhaps, instead of running convert-sequence on all sequences, it could be modified to run on all instances of content instead. [Edit: This is not possible, as show-rules don't work on the content-constructor]

~~Or the code could be refactored to actually use recursion, by mapping over seq.fields() instead of seq.children. Unless you exclude children matching .func == math.equation, the recursive solution has bad run-time for nested content: In the admittedly contrived example $a #[$b #[$c$]$]$, the expression b would have the quick-maths-transformation applied to it twice, and c three times. This is not the case for show-rules.~~ [Edit: This also is not possible: Mutation of content-fields is not allowed, and replacing a content-object by a mutated copy not feasible, because positional arguments of content-functions mustn't be passed as named arguments:

  let returnCopy(contentt) = {
    return contentt.func()(..contentt.fields())
    // This doesn't work, either: 
    // return contentt.func().with(..contentt.fields())()
  }
  returnCopy($123$) // "⚠ The argument `body` is positional`

]

Replacement of non-maths content

In the current implementation, all sequences within a math.equation are replaced. This includes sequences of non-maths-content, in particular strings. This seems unsolvable at the moment, because we seem unable to distinguish between strings and maths-content in math-mode:

#let children = $a "a"$.body.children // ([a], [ ], [a])
#(children.at(0).fields() == children.at(2).fields()) // true
#(children.at(0).func() == children.at(2).func()) // true
#(children.at(0) == children.at(2)) // true

This currently is a small issue, because string-contents aren't broken apart like equation-contents are (#$a+b "a+b"$.body.children yields ([a], [+], [b], [ ], [a+b])), so they usually don't match positively against math-content, and so aren't replaced. As a consequence, the example-bug below is extremely contrived. Depending on the resolution of the "Content that is not a sequence" in the previous section, this bug might actually cause problems in regular use, though.

Contrived example: Perhaps you type 1.5 a lot, but to be fancy, you would like it to be replaced by the fraction 3/2. Because $1.5$ is a single content-block and not a sequence, it currently can't be replaced by quick-maths, so you have quick-maths replace $1.5,$ instead. Now, you might use the number 1.5 in a string as well, e.g. to refer to figure-numbers. Of course, we wouldn't want to have the text "Looking at figure 1.5, we see…" be replaced by "Looking at figure ³/₂ we see…". In this example, that's what happens, though:

#import "@preview/quick-maths:0.1.0": shorthands
#show: shorthands.with(($1.5,$, $3/2$))

…depending on the sign of $n$, the claim follows from:
$
  "Figure" cases(
      "1.4"\, quad&"if" n<0,
      "1.5"\, quad&"if" n=0,
      "1.6"\, quad&"if" n>0,
  )
$
// Expected: The second line of the case-distinction
//           renders as "1.5, if n=0"
// Actual: The second line of the case-distinction
//         renders as "³/₂  if n=0"

Note that the comma after "1.5" only had to be backslash-escaped because it's within the arguments-list of the cases-function. In the even more contrived example $"From figure" "1.5", n=k$, the bug is triggered as well. Note that the bug is not triggered for $"From figure 1.5", n=k$ because here 1.5 is not an isolated string.

EpicEricEE commented 3 days ago

Sorry for the very late response, but I just pushed some new changes, which affect some of your comments:

lumi-a commented 2 days ago

Thank you for your response and for the improvements! I ran your tests and my above singleton-code against your new version, they all passed! Happy to close this issue :) 🌻