$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.
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 likeor
versus
So rather than add a
num_denomfunction, 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.,
Is it even possible to list all the globals that might affect the result?
There's a more compact notation:
That one should be recommended in the manual, if
num_denomis ever implemented.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.
Yes, it works in 5.50, you can try it here: https://maxima-on-wasm.pages.dev/
Caching is effectively impossible in Maxima. It's not just global variables that results depend on, but also property lists (think of
tellsimpchanging an operator'soperatorsproperty, or the facts database, which lives entirely inside property lists).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.
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 ...Of course Maxima bindings matter, but can Lisp-level
letetc. affect Maxima-level calls? Maybe inev?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
letorprogthat binds certain flags)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.Implementation, documentation and tests in commit [d52a14].
Still to be done: Refactor code that calls
$numand$denomon the same expression to call the new functionnum-denom-splitinstead, which has the same interface asrisplit.Related
Commit: [d52a14]
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.Agreed. I chose the dotted pair for consistency with
risplit. I'll switchnum-denom-splitto(values num denom)now, while all callers are in two fresh commits.Now using multiple values, closing this.
Maybe the discussion about caching could be continued on the mailing list.