Menu

#5257 Provide $num_denom and use it internally

None
closed
nobody
num (1) denom (1)
5
6 days ago
2026-09-15
No

$num and $denom are often called together to split an expression into numerator and denominator. It is potentially expensive due to the internal nformat call. The suggestion is to provide a combined function num_denom(x) that returns [num(x), denom(x)] more efficiently. It should then be used internally where currently both $num and $denom are called on the same expression.

Discussion

  • Stavros Macrakis

    I agree that pairs of functions like num/denom, realpart/imagpart, etc. are often used together and are computationally wasteful. However, I think they're more compact, easier to write, and clearer than something like

    block([numdem: num_denom(x)],
        if oddp(numdem[2]) then abs(numdem[1])/numdem[2])
         ...
    

    or

    block([numdem: num_denom(x), num, denom],
        num: numdem[1], denom: numdem[2],
        if oddp(denom) then abs(num)/denom)
         ...
    

    versus

        if oddp(denom(x)) then abs(num(x))/denom(x)
    

    So rather than add a num_denom function, what about making the functions more efficient?
    The obvious way is to add a cache of recent values. Alas, this is not quite as easy as one might like in Maxima (is anything?) because there may be global switches which could affect the result and which could change between the two calls, e.g.,

    realpart(exp(%i*x)) => cos(x)
    block([exponentialize:true],realpart(exp(%i*x)))
          => (%e^(%i*x)+%e^-(%i*x))/2
               (correct, even if it's not free of %i)
    

    Is it even possible to list all the globals that might affect the result?

     
    • David Scherfgen

      David Scherfgen - 2026-09-15

      There's a more compact notation:

      block([[num, denom] : num_denom(x)], ...);
      

      That one should be recommended in the manual, if num_denom is ever implemented.

       
      • Stavros Macrakis

        That isn't supported in 5.49.
        But maybe it's in a more recent version? -- I need to update to 5.50 I guess.

        block([[n,d]:[4,5]],n+d);
        Only symbols can be bound; found: [n,d]
        
         
        • David Scherfgen

          David Scherfgen - 2026-09-15

          Yes, it works in 5.50, you can try it here: https://maxima-on-wasm.pages.dev/

           
    • David Scherfgen

      David Scherfgen - 2026-09-15

      Caching is effectively impossible in Maxima. It's not just global variables that results depend on, but also property lists (think of tellsimp changing an operator's operators property, or the facts database, which lives entirely inside property lists).

       
      • Stavros Macrakis

        A simple approach to safe caching is to increment a global-state counter.
        Any side-effect operation -- assignment to a global variable (not just flags), redefinition of a function, declaration or assumption, etc. -- would increment the counter.
        I suspect that in the vast majority of cases, that would save a recalculation.

        Is it worth it? Not sure. But with the help of Claude, I'd think it would be pretty easy to replace all instances of (setq $xxx ...) with (gsetq $xxx ...) etc. where gsetq takes care of the state counter.

         
        • David Scherfgen

          David Scherfgen - 2026-09-15

          Yes, that's a possibility. It also has to cover bindings done via let, prog, &aux, ... Symbol property lists being modified destructively through references, ... Quite a lot of ways how the state can be modified, often not obvious by looking at the code, and from thereon all future code modifications have to adhere to the protocol or risk introducing subtle bugs ...

           
          • Stavros Macrakis

            Of course Maxima bindings matter, but can Lisp-level let etc. affect Maxima-level calls? Maybe in ev?

             
            • David Scherfgen

              David Scherfgen - 2026-09-16

              Ah, you're suggesting that only user-level calls be cached, and not those done by internal code? (as you know, internal code also calls Maxima-level (user-level) functions, often inside a let or prog that binds certain flags)

               
              • Stavros Macrakis

                Internal code should be using risplit (the other bug report). Not sure why Lisp code would be calling $num/$denom, which as you point out are expensive because they first format the expression. But of course both of these are possible, just as it's possible that Lisp code isn't following other conventions like always returning simplified expressions. I suppose that you have been cleaning up a lot of these taking advantage of Claude.

                 
  • David Scherfgen

    David Scherfgen - 7 days ago

    Implementation, documentation and tests in commit [d52a14].
    Still to be done: Refactor code that calls $num and $denom on the same expression to call the new function num-denom-split instead, which has the same interface as risplit.

     

    Related

    Commit: [d52a14]

    • Stavros Macrakis

      Lisp didn't have multiple value returns when I wrote risplit (1971 or so), and memory was considered expensive, hence the dotted pair. Multiple values seem like the right way to do it today, and are even more efficient! Also, dotted pairs of Maxima expressions don't print very nicely.

       
      • David Scherfgen

        David Scherfgen - 6 days ago

        Agreed. I chose the dotted pair for consistency with risplit. I'll switch num-denom-split to (values num denom) now, while all callers are in two fresh commits.

         
  • David Scherfgen

    David Scherfgen - 6 days ago
    • status: open --> closed
     
  • David Scherfgen

    David Scherfgen - 6 days ago

    Now using multiple values, closing this.
    Maybe the discussion about caching could be continued on the mailing list.

     

Log in to post a comment.