Dealing with money is important, even if it's only a small part of mathematics. I'll focus on that.
Python's 'decimal' module uses overloaded operators so you can do things like:
from decimal import Decimal as D
tax_rate = D('0.0765')
subtotal = 0
for item in purchase:
subtotal += item.price * item.count # assume price is a Decimal
taxes = (subtotal * tax_rate).quantize(D('0.00'))
total = subtotal + taxes
Plus, there's support for different rounding modes and precision. In Python's case, something like "a / b" will look to a thread-specific context which specifies the appropriate settings:
>>> import decimal
>>> from decimal import localcontext, Decimal as D
>>> D(1) / D(8)
Decimal('0.125')
>>> with localcontext(prec=2):
... D(1) / D(8)
...
Decimal('0.12')
>>> with localcontext(prec=2, rounding=decimal.ROUND_CEILING):
... D(1) / D(8)
...
Decimal('0.13')
Laws can specify which settings to use, for examples,
https://www.law.cornell.edu/cfr/text/40/1065.20 includes "Use the following rounding convention, which is consistent with ASTM E29 and NIST SP 811",
(1) If the first (left-most) digit to be removed is less than five, remove all the appropriate digits without changing the digits that remain. For example, 3.141593 rounded to the second decimal place is 3.14.
(2) If the first digit to be removed is greater than five, remove all the appropriate digits and increase the lowest-value remaining digit by one. For example, 3.141593 rounded to the fourth decimal place is 3.1416.
... (I've left out some lines)
and from
https://www.law.cornell.edu/cfr/text/7/1005.83 :
(3) Divide the result in paragraph (a)(2) of this section by 5.5, and round
down to three decimal places to compute the fuel cost adjustment factor;
(4) Add the result in paragraph (a)(3) of this section to $1.91;
(5) Divide the result in paragraph (a)(4) of this section by 480;
(6) Round the result in paragraph (a)(5) of this section down to five decimal
places to compute the mileage rate.
There's probably laws which require multiple and different rounding modes in the calculation.
This means simply doing all of the calculations in scaled bigints or as fractions won't really work.
Now of course, you could indeed handle all of this with prefix functions and with explicit context in the function call, but it's going to be more verbose, and obscure the calculation you want to do. I mean, it's not seriously worse. Compare:
with localcontext(prec=3, rounding=decimal.ROUND_DOWN):
line3 = line2 / D("5.5")
line4 = line3 + D("1.91")
line5 = line4 / 480
line6 = line5.quantize(D('.00001'), rounding=decimal.ROUND_DOWN)
vs. some function-based API with overloaded parameter types:
line3 = decimal_div(line2, D("5.5"), prec=3, rounding=decimal.ROUND_DOWN)
line4 = decimal_add(line3, D("1.91"))
line5 = decimal_div(line4, 480)
line6 = decimal_quantize(line5, D('.00001'), rounding=decimal.ROUND_DOWN)
But it is worse. I also originally made a typo in the function-based API for line5 where I used "decimal_add" instead of "decimal_div" - the symbols "/" and "+" stand out more, and are less likely to be copy&pasted/auto-completed incorrectly.
If overloaded parameters - "spooky action at a distance vibes" - also aren't allowed, then this becomes more rather more complicated.