SymbolicUtils.jl

Interfacing with SymbolicUtils.jl

This section is for Julia package developers who may want to use the simplify and rule rewriting system on their own expression types.

If not directly using @syms and methods defined on symbols, the easiest way to interface with SymbolicUtils is to convert your symbolic types into SymbolicUtils' types, perform the desired rewrites, and convert back to the original types.

This may sound like a roundabout way of doing it, but it can be really fast. In our experements with using this package to impliment simplification for ModelingToolkit.jl the conversion accounted for about 3% of the total time taken for simplify. This approach also means that you don't ahve to take for face value the assumptions and reservations of SymbolicUtils.

Defining the interface

SymbolicUtils matchers can match any Julia object that implements an interface to traverse it as a tree.

In particular, the following methods should be defined for an expression tree type T with symbol types S to work with SymbolicUtils.jl

istree(x::T)

Check if x represents an expression tree. If returns true, it will be assumed that operation(::T) and arguments(::T) methods are defined. Definining these three should allow use of simplify on custom types. Optionally symtype(x) can be defined to return the expected type of the symbolic expression.

operation(x::T)

Returns the operation (a function object) performed by an expression tree. Called only if istree(::T) is true. Part of the API required for simplify to work. Other required methods are arguments and istree

arguments(x::T)

Returns the arguments (a Vector) for an expression tree. Called only if istree(x) is true. Part of the API required for simplify to work. Other required methods are operation and istree

In addition, the methods for Base.hash and Base.isequal should also be implemented by the types for the purposes of substitution and equality matching respectively.

Optional

similarterm(t::MyType, f, args)

Construct a new term with the operation f and arguments args, the term should be similar to t in type. if t is a Term object a new Term is created with the same symtype as t. If not, the result is computed as f(args...). Defining this method for your term type will reduce any performance loss in performing f(args...) (esp. the splatting, and redundant type computation).

The below two functions are internal to SymbolicUtils

symtype(x)

The supposed type of values in the domain of x. Tracing tools can use this type to pick the right method to run or analyse code.

This defaults to typeof(x) if x is numeric, or Any otherwise. For the types defined in this package, namely T<:Symbolic{S} it is S.

Define this for your symbolic types if you want simplify to apply rules specific to numbers (such as commutativity of multiplication). Or such rules that may be implemented in the future.

promote_symtype(f, arg_symtypes...)

Returns the appropriate output type of applying f on arguments of type arg_symtypes.

Example

Suppose you were feeling the temptations of type piracy and wanted to make a quick and dirty symbolic library built on top of Julia's Expr type, e.g.

for f ∈ [:+, :-, :*, :/, :^] #Note, this is type piracy!
    @eval begin
        Base.$f(x::Union{Expr, Symbol}, y::Number) = Expr(:call, $f, x, y)
        Base.$f(x::Number, y::Union{Expr, Symbol}) = Expr(:call, $f, x, y)
        Base.$f(x::Union{Expr, Symbol}, y::Union{Expr, Symbol}) = (Expr(:call, $f, x, y))
    end
end


ex = 1 + (:x - 2)
:((+)(1, (-)(x, 2)))

How can we use SymbolicUtils.jl to convert ex to (-)(:x, 1)? We simply implement istree, operation, arguments and we'll be able to do rule-based rewriting on Exprs:

using SymbolicUtils

SymbolicUtils.istree(ex::Expr) = ex.head == :call
SymbolicUtils.operation(ex::Expr) = ex.args[1]
SymbolicUtils.arguments(ex::Expr) = ex.args[2:end]

@rule(~x => ~x - 1)(ex)
:((-)((+)(1, (-)(x, 2)), 1))

However, this is not enough to get SymbolicUtils to use its own algebraic simplification system on Exprs:

simplify(ex)
1 + (x - 2)

The reason that the expression was not simplified is that the expression tree is untyped, so SymbolicUtils doesn't know what rules to apply to the expression. To mimic the behaviour of most computer algebra systems, the simplest thing to do would be to assume that all Exprs are of type Number:

SymbolicUtils.symtype(s::Expr) = Number

simplify(ex)
1 + (x - 2)

Now SymbolicUtils is able to apply the Number simplification rule to Expr.