Mathemagics

Description

Symbolic algebra package written in Pharo Smalltalk

Details

Source
GitHub
Dialect
pharo (25% confidence)
License
MIT
Stars
26
Forks
4
Created
Dec. 4, 2020
Updated
July 17, 2025

Categories

Math

README excerpt

# Mathemagics

## Symbolic algebra package written in Pharo Smalltalk

Pharo Smalltalk is an expressive and powerful language, but when you just working with it, some things are rather puzzling:

* There is no built-in Calculator application. Smalltalk have the Playground but sadly the precedence of operands does not fully match Mathematics precedence (`1 + 2 * 3` returns 9 rather than 7).
* Missing leading zero `.45` is not supported although most people in finance are used to omit it, and other languages take that as default.
* Writing a number in scientific notation does not support decimal exponents `3e2.5` (Smalltalk returns 5).
* Some numbers become incorrect, for instance `3e-4` becomes `0.00030000000000000003`.
* Power and square are not quite compatible therefore `25 sqrt` returns `5` but `25 ** 0.5` returns `4.999999999999999`.
* Other cases such as `(Float pi / 2) tan` would return a huge number rather than 'Undefined' because `Float halfPi cos` is not exactly zero, but `Float halfPi sin` returns a perfect `1.0`. These results are confusing specially since Smalltalk can handle cases like `(1/3)*3` => `1` which other languages cannot handle.
* Smalltalk provides a way to simulate a mathematical function using a block `f := [ :x | 1 / x ]` which is quite neat. The block may get more complex if you use a function that is undefined for some values, such as Logarithm of a negative number, or division by zero.

So I embark myself into experimenting with an implementation of symbolic algebra for Smalltalk. Mathematics is a huge language in itself, and therefore there is an implicit scope on this project, but most algebra and a little of calculus has been implemented. New functions can be added with very little effort and without changing other classes, since the design contemplates ambiguity (unary or multiple function names), precedence, Smalltalk vs Math names, partial name match (#sqrt vs #sqr) and other problems have been solved.

## Why is called 'Mathemagics'?

Originally the package was meant to be called MathEx (short for Mathematical Expressions, and to match RegEx) but nowadays there is more functionality already available. Apart from Mathematical Expressions, the package has a parser, a calculator, a repository for formulas, and more to come in the future.

## Ok.. but what can I do with it?

A few examples of things you can do:

* Building expressions with Number-like messages

```Smalltalk
  "Notice ** gets replaced by ^ on printing, both accepted"
  MathExpVariable x ** 3 negated                                "x^-3"
```

* Building expressions with the new built-in parser

```Smalltalk
  MathExpressionParser new parse: 'x + 2'.                      "x + 2"
```

* Parse via expression, simplify, sort terms, use symbols (pi => π)

```Smalltalk
  | fx |
  "Notice pi gets represented with a symbol"
  fx := MathExpression from: 'x*x - (pi * -1) + x*8/1'.
  fx simplify.                                                  "x^2 + 8·x + π"
```

* Nested behaviour on simplification even on functions

```Smalltalk
  | fx |
  fx := MathExpression from: 'sin(x)*cos(x+x)/sin(sqr(x)/x)'.
  fx simplify.                                                  "cos(2·x)"
 ```

* Derivatives

```Smalltalk
  | fx dFx |
  fx := MathExpression from: 'x*x + ln(x)'.
  dFx:= fx derivativeFor: 'x'.                                  "2·x + 1/x"
```

* Variable extraction and evaluation

```Smalltalk
  | fx x |
  fx := MathExpression from: 'sqrt(x) + x*x*x + 1/x'.
  fx := fx simplify.                                            "√(x) + x^3 + 1/x"

  "expression with variables are not reduced to a Number"
  fx isNumber.                                                  "false"

  "Extract variable and evaluate for a number"
  x := fx variable: #x.
  x value: 5.
  fx asNumber.                                                  "127.43606797749979"
```

* Block generation

```Smalltalk
  "Expression to a Smalltalk BlockClosure"
  | fx x |
  "Math precedence means no parenthesis required"
  fx := MathExpression from: 'x + 3 * pi'.                      "x + π·3"

  "New method will generate code with Smalltalk precedence"
  fx asBlock                                                    "[ :x | x + (3 * Float pi) ]"
```

## What are the main components of this package?

There are 4 basic components:

1. **MathExpression:** *This class and its subclasses can represent any mathematical expression the user can write using a number, mathematical constants, operators and functions. A complex expression is built as a tree of mathematical expressions. This class is aligned with the Number class, so you will find familiar messages such as `+`, `#square`, `#isNumber`, `#asNumber`, `#positive`, etc. On top of those message you will find new functionality such as `#asPower`, `#simplify`, `#derivativeFor:`, `#dividend`, `#isFunction` and others. The subclasses prefix are shorten to 'MathExp' for simplicity reasons, but you shouldn't need to use them directly.*
   - Unary expressions (sin, cos, abs)
   - Binary expressions (power, log, addition)
   - Value holders (numbers, mathematical constants, variables)
   - Dependencies: `String`, `Number`, `Set`, `Dictionary`

2. **MathExpressionParser:** *This class analyses a text and return a MathExpression representing it. The parser uses Maths precedence (not Smalltalk precedence). The parser has a fully implemented non-greedy Infix notation (and it should be possible to extend for Postfix or Polish notations).*
   - Parser fits in just 1 class with barely 30 short methods
   - Dynamic analysis of MathExpression hierarchy determines parser capabilities
   - Uses Regular Expressions instead of parser generators to avoid external dependencies
   - The underlying implementation algorithm based on is Shunting Yard
   - Instantiates objects using `#perform` to run operations same way the user can do
   - Dependencies: `MathExpression` and subclasses, `String`, `Number`, `Dictionary`, `Regex`

3. **Calculator:** *This is a proxy to a simple Spec1 application (in the future can decide to call a Spec2 instead). It mainly contains an edit area, a results area, and a basic toolbar. The user types the expression so there is no need of buttons as in common calculators.*
   - Uses the parser and holds MathExpressions with their results
   - Displays the expression as close as possible the user entered (settings available)
   - Simplify the expression if possible (e.g.: User enters `x+x` it will display the input and `= 2*x`)
   - Evaluates the expression to a Number if possible (e.g.: User enters `2+pi+2` it will print `pi+4 = 7.1415..`)
   - It allows copying to clipboard, inspection and manipulation of results
   - Dependencies: `MathExpression`, `MathExpressionParser`, `Spec1` (atm).

4. **Formula Library:** *The idea is to have a simple repository of commonly used formulas that can be reused in the future.*
   - Class protocols becomes a classification mechanism: `Financial`, `Geometrical`, `Mathematical`, etc.
   - Formula parameters can be numbers or variables for further evaluation
   - Dependencies: `MathExpression`

## Highlighted features

Here are some of the most interesting methods available:

* `#isNumber`
    Answers if the expression can evaluate to a Smalltalk Number (expression has no variables unset and not undefined values).
* `#asNumber`
    Returns the numeric result of the expression as Smalltalk Number (including Fraction).
    Notice that square root provides one answer (`√25` = `+5`) but Math has two solutions (`-5`).
* `#simplify`
    Answers a new expression if the current one can be reduced to a simpler form.
    The new form is meant to be simpler to read. Long numbers are avoided.
* `#derivativeFor:`
    Answers a new expression that is the derivative for a Variable of the current expression.
* `#asBlock`
    Returns a BlockClosure that can be use inside other code such as Roassal.
* `#replace: with:`
    Replaces occurrences of the subexpression with another one.
* `#variables`
    Returns a list of all the variables in the expression.
* `#variable:`
    Returns the variable that has the specified symbol.
* `#sqr`
    Answers an expression raised to 2 using sqr function (non-standard)
* `#square`
    Answers an expression raised to 2.
* `#cubed`
    Answers an expression raised to 3 (non-standard)

## Examples using the package

* Calculator

    Editing formulas.

    ![Calculator](resources/screenshots/Calculator-screenshot-1.jpg)

    Evaluating variables in a formula:

    ![Variables](resources/screenshots/Calculator-screenshot-2.jpg)

* Using the library with Roassal

    Demo code (text):

    ```Smalltalk
    | fx s p chart |

    fx := MathExpression from: 'pi-sin(abs(x))'.

    s := -10 to: 10 count: 1000.

    p := RSScatterPlot new.
    p color: (Color fromString: 'ce7e2d').
    p x: s y: (s collect: fx asBlock).     "generate block on the fly"

    chart := RSChart new.
    chart addDecoration: RSHorizontalTick new.
    chart title: 'Roassal & Mathemagics'.
    chart xlabel: fx asString.             "print the expression Math friendly"
    chart ylabel: fx asBlock asString.
    chart addPlot: p.
    chart open.
    ```

    Produces this result:

    ![Roassal](resources/screenshots/Roassal-screenshot-1.jpg)

    Another example with generated derivative:

    ![Roassal](resources/screenshots/Roassal-screenshot-2.jpg)

## Goals

Some of the reasons for the design:

**For MathExpression**
- [x] Represent any expression including variables ('x', 'y', etc) and mathematical constants (pi, e, phi), operands and functions
- [x] Evaluate an expression to a number when needed not only during creation
- [x] Allow to enter the expression and represent as closely as entered (e.g.: `sqr(25)` does not suddenly convert to `5`)
- [x] Support Simplification of an expression (e.g.: `x+x` => `2·x`)
- [x] Support Differentiation of an expression (e.g.: `(x^3)` derivative is `3·x^2`)
- [x] Print
← Back to results