Interesting consequence of current parsing rules
Q: Without typing it into the mathjs.org REPL, what type is passed as the argument to not in
math.evaluate('not(sparse([[1, 1]])).valueOf()')
and what is the result of
math.evaluate('typeOf(sparse([[1, 1]])).valueOf()')
???
A: Turns out that in the first case, the type is Array, whereas in the second, it's SparseMatrix. In other words, the first expression is parsed as not( (sparse([[1, 1]])).valueOf() ) whereas the second is parsed as (typeOf(sparse([[1, 1]]))) .valueOf(), even though they look syntactically identical. (This difference led to a bug in a mathjs expression that took me a couple of hours to figure out, unfortunately.) The difference it seems stems entirely from the arbitrary designation that not is a unary operator, as opposed to typeOf that is designated as a unary function (even though, for example, in JavaScript typeof is an operator).
I've suggested before that this distinction between unary operators and functions is artificial and has no benefit and that the expression language would be improved by eliminating it. I am not posting particularly to reopen that debate. I think the important thing for now is that when an operator is used with function-style syntax not(expr) it should parse/behave like a function, just as and(exprA, exprB) does. Otherwise, it's just too difficult to read and write expressions, having to remember special rules for just a small handful of specific symbols. I would suggest that the current non-parallelism between not and typeOf amounts to something at least very close to bug, and would suggest it be addressed in one or more of the following ways:
I) Just make not a unary function. The only side effect is that with no other changes to the parser, the expression not true would become illegal, and one would have to write not(true).
II) Allow any unary function (e.g., sin) to apply to the following expression like not does now (i.e., in essence make all unary functions into unary operators). I know @josdejong has had concerns about this in the past; I am merely including it here to record the logical possibilities I can think of.
III) Alter the parser so that when a function or operator is used with unary function-call notation, OP(EXPR), then it parses just like a unary function call, with the same precedence and grouping, etc.
IV) Try for a one-off fix for the specific troublesome expressions noted above, e,g., try making the not operator higher-precedence than the . accessor operator, so that not S.method() always parses as (not S).method() rather than not (S.method()), even without any parentheses, This precedence change would be at odds with JavaScript, where !object.method() means !(object.method()) rather than (!object).method().
V) Do nothing except document the difference between a unary function and a unary operator even more clearly, with a specific warning about the pitfall of this parsing difference between the two.
There may of course be other options I haven't thought of., and clearly I am not a fan of (V). But please just let me know what direction you'd like to go in connection with the potential difficulty highlighted here and I will file a PR as soon as I can.
Thanks for reporting. Sorry to hear that this bug took so much time to figure out :(
Just for the record, your examples currently evaluate as:
math.evaluate('not(sparse([[1, 1]])).valueOf()') // Array [false, false]
math.evaluate('typeOf(sparse([[1, 1]])).valueOf()') // String "SparseMatrix"
I think the issues here does not originate from unary operators vs functions, but from which functions evaluate matrix input element-wise. Many functions like not (and abs, log10, and unaryPlus, etc) evaluate matrices and arrays element-wise. But the function typeOf does not evaluate element-wise: it returns a string with the name of the data type of the input. Or do I misunderstand your case?
I think you misunderstand the case. Check the parse trees. The first example parses as not applied to the expression (sparse([[1,1]])).valueOf() and the second parses as the .valueOf() accessor call on the result of typeOf(sparse([[1,1]])). So it is a precedence difference resulting from not as an operator vs typeOf as a function. But it's a very deceptive one because except for the token substitution not vs typeOf, they are syntactically identical, so it is surprising to the point of a bug that they should end up with very different parse trees. How should we deal with this difference? I think it should be eliminated in some way, personally.
I think another way of putting the difficulty is that in not(sparse([[1,1]])).valueOf(), no sparse matrix is ever negated -- .valueOf() is called on the result of (sparse([[1,1]])) before it is passed to not, which I think is very surprising to the point of a bug, and is the opposite of what happens in typeOf(sparse([[1, 1]])).valueOf(), which is why you get the answer SparseMatrix, not Array in the latter case. If in the former case you force the not to apply to the sparse matrix with (not(sparse([[1,1]]))).valueOf() you will see that the not applied to the sparse matrix returns [[0, 0]] rather than [[false, false]] because of bug #3609.
A related phenomenon i just found that definitely is a bug: 2 size(false) evaluates to [] (which is2*[]), whereas 2 not(false) is a syntax error, whereas to be parallel it should be 2*true i.e. 2.
I think any of the approaches to the parse difference originally reported, except (V) just document the nonparallelism, would fix this bug. EDIT: actually i don't think IV would work for this either -- hard to see how just changing precedence could eliminate a syntax error. So I would recommend one of I, II, or III, unless you have another approach you can come up with.