Designing a Cursed Calculator App
Recently, a friend of mine1 at university started a Discord server where we will be having programming projects every few weeks. The first project that was posted, was to create a calculator program.
Now, a simple PEDMAS command-line calculator is something I've done plenty of times before, though adding some functions like sin and cos might be interesting, but ultimately not more than a day of two of work, given my prior experience.
But a calculator needn't necessarily be so boring. What about allowing Reverse Polish notation? What about allowing juxtaposition for multiplication? Or defining your own functions?
And if you're at the point where you're defining your own functions, you might as well make the calculator Turing-complete. Heck, what is a calculator, other than a numerics-focused programming language?
So here I'm going to flesh out a bit of the syntax of my proposed calculator language.
First, I want to support user-defined functions, as well as three syntax modes: normal infix mode with order of operations, suffix mode (Reverse Polish), and prefix mode (almost like lisp, but without the brackets).
First, let us consider a possible equation this calculator might accept:
6*7 + sin(5a)/fib(10)
This might decompose into the following AST2:
+
/ \
* /
/ \ / \
6 7 sin fib
| |
(*) 10
/ \
5 a
And the final result should be 42 + [sin(5*a)/55], which would of course depend on the value of the variable a.
Now how might this look in prefixed or postfixed notation? I thought about it for a bit, and came up with the following, which demonstrates a bit of a cursed idea I had:
prefixed: + * 6 7 / sin 5a fib 10
postfixed: 6 7 * 5a sin 10 fib / +
Did you notice the cursed bit? Well, there are actually two such elements, the one a bit less cursed than the other. The first, less cursed, one is that in a certain sense functions change the syntax in prefixed form: rather than requiring parentheses around function application to indicate how many arguments the function takes, as one would in lisp, each function or operation takes a fixed number of arguments – two here in the case of operations, and one in the case of functions. This implies that if you had a two- or three-argument function, it would implicitly consume the following two or three values.
The more cursed bit is that I still have multiplication via juxtaposition. At first I thought that I couldn't really support such a feature, as writing a number and then a variable name would correspond to two values, which would be consumed as inputs to the functions or operators. But when I thought about it for a bit, I could make white space significant, and have something like 5 a be the value 5 followed by the value of a, and 5a be five times a. This certainly is strange, and perhaps unintuitive, but on further reflection isn't that weird: after all, is 6 7 not different from 67?
Then there is something else I was thinking of, but which isn't demonstrated here: I was talking just now to the guy who's presenting the project, and in his prototype calculator program, he allows juxtaposition multiplication of variables, such that abc is equivalent to (a*b*c). This has the downside of only allowing single-letter variable names (barring certain workarounds), which I'm not personally a big fan of, so initially I thought that I would want to avoid this in my language.
But I have hit upon a solution. A cursed solution, but a solution none the less.
Just let user-defined variables and function change the syntax. That is, when a variable or function is defined, it is added as a possible token, such that if the variables a, pi, fib, and foo are defined, then apifib(foo) would be tokenised as "a" "pi" "fib" ( "foo" ), rather than "apifib" ( "foo" ) or "a" "p" "i" "f" "i" "b" ( "f" "o" "o" ).
Now, for any serious programming language this would be an awful idea. But then again, this isn't a programming language, is it? It's a calculator. A calculator I might want to make Turing complete, but still a calculator. And this sort of makes sense, doesn't it, almost feels like how you would expect a calculator to behave. To some degree, desmos has a similar thing going, where asin(5bc) is equivalent to a*sin(5*b*c), though of course it only has pre-defined multi-character names, with all user-defined names (functions and variables) having to be single characters.
Some other considerations
There still are a few things I need to flesh out a bit in my mind. Stuff like how function/variable definition/assignment will look.
I could do something like a = tau/12 and fib(n) = ..., but how would this look in prefix or postfix mode? Do I just ban definition/assignment in those modes? That strikes me as inelegant3. As does having some sort of drastically different syntax in different modes.
Furthermore, I'm wondering about how exactly I'll allow function definitions. Perhaps something functional with pattern matching would be nice, so that I could define the Fibonacci function as follows:
f(0) = 0
f(1) = 1
f(n) = f(n-1) + f(n-2)
And I think I probably shouldn't allow arbitrary for or while loops, as that feels too much like a programming language rather than a calculator.
But then, how would I allow something like an efficient implementation of functions such as the Fibonacci function? After all, the given implementation above runs in about O(n²) time, iirc. For this I can think of two possible solutions.
One is to automatically apply memoization to all functions; if just the previous two values of the Fibonacci function is memoized, it now runs in O(n) time!
Another way would be to allow lambdas – for which choosing the syntax will be its own little adventure – such that you could instead define the Fibonacci function as follows4:
f(n) = (\(a b n) if n = 0 then a else rec(b, a+b, n-1))(0, 1, n)
I've also been considering allowing parentheses to be dropped for single-argument functions. This means that fib(10) and fib 10 would be the same. And theoretically something like afibb would be the same as (a*fib(b)), though perhaps juxtaposition-looking function application should instead be a syntax error. I was also wondering if all multi-argument functions should just require parentheses for function application (in the infix mode), or if I should let two-variable functions also operate as infix operations, so that I can have 2 foo 3 be equivalent to foo(2, 3)5, though now the question becomes of how to slot that into operator precedence rules.67
*(6, 7) would be the same as 6 * 7...foo 2, 3 being equivalent to foo(2, 3), but I thought of it for a bit, and it presents too many complications and is quite ugly in my opinion, so I will not have thatfoo(2, 3) is equivalent to foo(2)(3), which would then be equivalent to foo 2 3; this seems quite elegant, other than you'd now have to work out how exactly it interacts with multiplication via juxtapositionOne last consideration is how to deal with operators which could take either one or two arguments. That is, plus and minus. In infix form, it is easy: - 6 takes one argument, 8 - 6 takes two. And in 5 - + 4, the minus takes two arguments and the plus one argument. But in prefix and suffix forms, this no longer is obvious. One way of resolving it is by going the APL route, and, for example, use - only for binary subtraction and use something like _ for unary negation. But this doesn't seem so nice; I want a user to be able to input a sum in a relatively intuitive way into the infix mode and have it work, and having some sort of secondary operation for negation would break that. One possible solution is to expand - x to (0 - x), but this seems a bit inelegant. But I'm thinking of it.