Open Happypig375 opened 6 years ago
@Happypig375, thanks for the insightful review. A few points:
Item 3 is only a temporary fix. I guess it would improve readability but it still doesn't eliminate clone being called unnecessarily. The ideal solution would be just that, to determine when clone is needed. My initial thought was that cloning wasn't need at all but I quickly learned otherwise and this patch has been the running solution since.
Item 6 would be another one that would be ideal but due to limitations in JavaScript numbers and the need to use a BigNumber library, the results haven't been the greatest. The problem is worsened by the fact that nerdamer uses the Frac library which uses a bigInt for both the denominator and the numerator. Just imagine what happens when we get a really tiny number for instance. The denominator get HUGE quickly in an infinite series. Even with a low number of iterations.
Item 7 is on the TODO list for the parser rewrite and has given me much grief. I suspect that it might take more than one iteration to get this just right but let's see.
I'm going to be re-reading this issue since there's much to consider and it's pretty long :smile:
I'm going to be re-reading this issue since there's much to consider and it's pretty long 😄
And the amount of consideration have taken me 2 months to write this 😛
Hi @jiggzson do you have any updates for this issue?
@ibreaker14, yes. I'm going to be starting this shortly.
0. TL;DR
This proposal is my take on supporting arbitrary precise decimals, through a new BigDecimal data type along with library support for its operations. Bonuses included. Also fixes buildFunction().
1. Problem
Nerdamer uses native JavaScript Numbers which restricts Nerdamer's ability to achieve a high decimal precision. This proposal attempts to solve that using a BigDecimal data type which can store arbitrary precise decimals.
2. Objectives
3. Separation of non-mutating and mutating operations
i.e. Use non-mutating operations where possible; only use mutating operations where intermediate results are ignored or in performance-tight loops.
I propose the following in the Parser: a. Rename existing functions
add
,subtract
,multiply
,divide
,pow
andexpand
tomutatingAdd
,mutatingSubtract
,mutatingMultiply
,mutatingDivide
,mutatingPow
andmutatingExpand
. b. Investigate each line where these functions are used and determine if the mutating operations are really needed. If so, change them to usemutatingAdd
etc. c. Insert new non-mutating functions, e.g.The SAFE flag of Settings and blocks of SAFE code can now be removed.
4. The BigDecimal structure
This is the classical structure of every real number data type out there. The mantissa and the exponent store bigInts to achieve arbitrary precision.
The precision of calculations will be specified at Settings.PRECISION, with negative meaning number of significant figures, and number of decimal places otherwise.
Here are some functions to help out inside the arithmetic functions:
5. Reusing Parser operations
Currently the Parser operations support Symbols, Vectors and Matrices. I propose that BigDecimals (, Fracs and bigInts)[OPTIONAL] should also be supported.
For example:
5.1. BigDecimals
Add checks for BigDecimals, e.g.
Here are a few helper functions:
By the way, all calls like
_.parse('1')
ornew Symbol(1)
should be replaced withSymbol.integer(1)
.5.2. Fracs [OPTIONAL]
Move all instance functions to Parser, and convert them to BigDecimals if the other argument is BigDecimal.
5.3. bigInts [OPTIONAL]
May not need to actually mutate the arguments, see Section 5.4.
5.4. Determinism of the mutated argument of mutating operations
If we want the mutating behaviour be deterministic (preferred): The argument which is mutated should be determined by:
< Number< Frac [OPTIONAL] < Symbol (with no variables) < BigDecimal | bigInt [OPTIONAL]< Number< Frac [OPTIONAL] < Symbol (all groups)For
Numbers andbigInts, (choose one) a. When the arguments are NOT mutated despite using the mutating operations: The argument to be mutated is of type bigIntor Number. In this case,variable = _.mutatingMultiply(variable, ...)
cannot be reduced to_.mutatingMultiply(variable, ...)
. (preferred) b. Force mutation of bigInts by copying the implementation inside BigInteger.js, and force mutation of Numbers by using Number wrappers and converting every === on Numbers to ==:Pros and Cons: Pros: Further reduce the performance cost of clone()s by changing
_.add
to only clone when necessary. Enables the Parser operations to be further restructured to e.g._.add(a, b, mutate)
. Cons: Need to remember the rules of mutation (can reference here though); One small change in these operations can result in a lot of failing unit tests (is normal in a development process)If we want the mutating behaviour to be non-deterministic: All uses of mutating operations must be in the form
variable = _.mutatingMultiply(variable, ...)
. Pros: No need to remember the rules of mutation. Cons: Cannot effectively remove the performance cost of clone()ing.5.5. BigDecimal with variables
When a BigDecimal is operated with a Symbol with variables, (choose one) a. the Symbol is mutated with its group set to D (a new group) and disallowed to operate with other symbols; or b. the Symbol is mutated with its group set to DN (e.g. 2.33e2), DS (e.g. x^2.33e2), DEX (e.g. 2.332^x), etc. and are dealt separately; or c. a new DecimalSymbol is introduced as the return type and disallowed to operate with other symbols, not mutating any of the arguments; or d. the multiplier or power of the Symbol is assigned a BigDecimal and its group changed accordingly (need to go through checking that all code looking for Symbols will work).
5.6. Alternatives
Note: the mutating operations may need to be rethought if one of the following is adopted.
Alternative 1
Define all methods on Symbol as mutating and all methods on Parser as non-mutating. After restructuring,
a.add(b)
would mutate a and_.add(a, b)
would return a new Symbol for Symbols a and b.Alternative 2
Like Alternative 1 but the other way around.
a.add(b)
would return a new Symbol while_.add(a, b)
would mutate a.Note that both alternatives require rewriting of all code from Section 5 onwards.
5.7.
Numbers[ABANDONED]~~These are easy: Just use JavaScript operators or use Math functions. For conversion to Fracs, use
new Frac(number)
; for Symbols, usenew Symbol(new Frac(number))
; for BigDecimals, use~~~~. May not need to actually mutate the arguments, see Section 5.4.~~
6. Constructing via infinite series or infinite products
Since all elementary functions and most special functions are analytic, they can be defined as an infinite series. Therefore, BigDecimals should be able to be constructed from an infinite series or an infinite product. Construction from an infinite product is included in case it is needed.
Requires Frac.toDecimal to return a BigDecimal and this:
Frac.toDecimal should also have the second argument specified in the argument list to avoid misaligned new arguments later on.
Frac.toDecimal should support negative precision, aka significant figures.
7. Optimizations for constants
For constants like pi and e, I suggest a BigDecimalGenerator structure:
The decimal generators of constants like pi and e will be stored in the Parser. Whenever nerdamer evaluates an expression, the generate() method of each generator will be called and each of their values will be stored in the respective fields in MathD.
Like this: (Initialization)
(Whenever an expression is evaluated)
8. An arbitrarily precise Math object
The Parser currently both parses symbols and abstracts math operations. I propose to separate the math functions inside the Parser that can be made to only use abstract operations into Math2.
Therefore, the Parser must have functionality of all JavaScript math operators, and a new MathD object must be made to have functionality equivalent to the built-in Math object (and nothing more).
For functions that cannot return finite decimals, infinite series or infinite products should be used to provide decimal values when PARSE2NUMBER is set or evaluate() is called.
I propose that all functions in Math2 that cannot depend on Parser and MathD entirely be moved out into the Parser. This avoids the current near-spaghetti code that mixes logic when an integer is encountered and fraction is encountered.
The Math2 object should be entirely dependent on the Parser and MathD, not referencing any implementation details like Number, bigInt, Frac, Symbol or BigDecimal.
Question: Should the symbolic part of trig functions etc. be moved entirely into MathD or only the numeric part will be in there?
For example:
The Math2 object also should not use any mutating methods nor clone() for easier implementation of Section 12.
All functions in the following two sections would work on BigDecimals and have arbitrary precision, and never mutate the arguments.
8.1. JavaScript operations/functions to implement
Each implementation may rely on bigInteger.js, BigDecimal methods, fields of mathematical constants in MathD or functions that came before it. For simplicity's sake, BigDecimal.fromInfiniteSeries(function(k){...},0) is simplified to sum(...).
All the implementations should be used for PARSE2NUMBER scenarios in their symbolic equivalent.
Some functions may involve BigDecimal raised to an integer power. The method goes like this (No BigDecimal.simplify to increase speed):
function dpowi(x,k) { var c = x.clone(); c.mantissa = c.mantissa.pow(k); c.exponent = c.exponent.pow(k); return c; }
In my pseudo-implementations, I may mix JavaScript notation with math notation. The ^ operator always stands for _.pow and not _.xor.8.1.1. Problem with shifting operators
Should we allow decimals in shifting operators? This will resolve into one of the following:
a. Disallow decimals This is in accordance to most programming languages. Attempting to use decimals with shifting operators will result in an Error.
b. Silently convert to integers All decimals will be floored to an integer. 0.4<<1 will result in 0 while 2.2>>1 will result in 1.
c. Implement as multiplication or division of 2 0.4<<1 will be 0.8 while 2.2>>1 will be 1.1. This means that 1>>1 will be 0.5 instead of 0. Wolfram Alpha uses this option but it computes (0.1*10)>>1 as 0.5 while 1>>1 results in 0.
d. Treat decimals differently from integers 1>>1 will be 0 while 0.5<<1 will be 1. This also means that (0.5<<1)>>1 will be 0. So, (x<<1)>>1 cannot be simplified.
e. Leave as unevaluated Similarly to how Wolfram Alpha handles BitAnd[2.2, 1].
8.2. Functions to implement for the sake of communicating with external math functions (see Section 12)
8.3. Function not worth implementing (LOL)
8.4. Function for parsing a decimal
Maybe this can be called by _.parse when PARSE2NUMBER is true. This might be useless though.
8.5. Functions to import from BigInteger.js [BONUS]
There are a number of functions in BigInteger.js that are not part of nerdamer. Aside from implementing the functions from the built-in Math object, why not reference some from BigInteger.js too? :wink:
These should probably be able to be used from nerdamer itself, e.g. nerdamer("isEven(2)")
9. Exposing the API to Expression
The Expression class currently exposes add, subtract, multiply, divide and pow. Other math operations should be exposed too, including all of the functions in nerdamer that match with the ones in the JavaScript Math class (Sections 8.1, 8.2), along with the ones in Section 8.5.
The toDecimal method should return an Expression with a decimal symbol which behavior is as chosen in Section 5.5. Since the decimal symbol handles all the decimal-symbol interoperations, the Expression class can be used as is.
10. Text and LaTeX representation
Scientific notation for BigDecimals is defined as mantissa×10ᵉˣᵖᵒᵑᵉᵑᵗ, where a dot is inserted after the leading digit of the mantissa and the number of digits after the dot is added to the exponent.
Representations of decimal symbols should follow the scientific notation if its exponent after addition is out of the range [-5, 5], and display as normal if not.
The LaTeX for × is \times while the text for × is *.
For example (under precision over 6 decimal places or 7 significant figures):
When the mantissa is 1 or -1 and scientific notation is used, the 1 and the multiplication sign will be omitted. When the mantissa is 0, the entire BigDecimal will display as a good ol' egg.
Remember to bracket the decimal if it becomes the base of exponentiation and requires scientific notation to display, or the exponent of exponentiation when displayed as text.
The text and LaTeX described in this Section will be used for toString(), text(), toTeX() and latex() of decimal symbols and Expressions with them.
11. External math functions but with arbitrary precision
Let's create a new function called importFunction() and buff replaceFunction(). importFunction() takes the same arguments as replaceFunction() and exposes the given function to nerdamer("...") and nerdamer["..."] after the following enhancements.
Whenever a function is imported with one of the aforementioned two functions, it is parsed and all the operations will be replaced with calls to the Parser.
For example,
function(x,y){ return x+y; }
will be converted tofunction(x,y){ return _.add(x,y); }
before being consumed.This enables the following amazing scenario:
The function is transformed to:
This can potentially be simplified more by throwing
new Symbol('x')
andnew Symbol('y')
into it and observing the outcome.Now, we can enjoy symbolic and arbitrary-precise numeric output simply via
nerdamer("someFunc(1,2)")
. This can surely become a selling point for nerdamer.12. Interoperability with buildFunction
Now we can fix buildFunction(). Previously, if you did
nerdamer("sinc(x)").buildFunction()
, you'd getfunction(x) {var sinc = function (x) { if(x.equals(0)) return 1; return Math.sin(x)/x; }; return sinc(x); }
. Notice the equals(0).If we rewrite all the functions a bit to require Parser methods to be used, then our _.equals can be translated to
==
under the tables of Section 8.The tables of Section 8 should provide enough information to replace Parser methods to JavaScript native operations. Neat!
13. Related issues
221
269
357
394
14. Postscript
GG. Two months of work in one proposal (March 7 ~ May 5, 2018). This is totally unexpected by me :sob:
This one proposal has totally drained my motivation for new proposals/PRs. I am going to post this as the #404 Not Found special, as well as pre-celebration for my first year anniversary in here (August 10, 2018) :wink: I am going to post my to-do list as #405 then take a temporary break from nerdamer. I will still respond to new issues and conversations though. Goodbye :wave: