Working with Symbolic Expressions
By: Manning Publications
Tags: Math, Mathematics, Math For Programmers, Software Development, Programming

Solution: To save some typing, we can name our variables and square root function up front.

A = Variable('a')

B = Variable('b')

C = Variable('c')

Sqrt = Function('sqrt')

Then it’s a matter of translating the algebraic expression into the appropriate structure of elements and combinators. At the highest level, you can see this is a quotient of a sum (on top) and a product (on the bottom).

Quotient(

Sum(

Negative(B),

Apply(

Sqrt,

Difference(

Power(B,Number(2)),

Product(Number(4), Product(A,C))))),

Product(Number(2), A))

Mini-project: Create an abstract base class called Expression and make all of the elements and combinators inherit from it. For instance, class Variable() should become class Variable(Expression) . Then, overload the Python arithmetic operations +, -, *, and / to produce Expression objects. For instance, the code 2 * Variable(“x”) + 3 should yield: Sum(Product(Number(2), Variable(“x”)), Number(3)) .

Solution: See source code.

Putting a symbolic expression to work

For the function we’ve been studying this far, we wrote down a Python function that computes it:

def f(x):

return (3*x**2 + x)*sin(x)

As an entity in Python, this function is only good for one thing: returning an output value for a given input value x. The value “f” in Python doesn’t make it particularly easy to programmatically answer the questions we asked at the beginning of the article, like whether “f” depends on its input, whether “f” contains a trigonometric function, or what the body of “f” would look like if it were expanded algebraically. In this section we’ll see that once we’ve translated the expression into a data structure in Python, built from elements and combinators, we can answer all of these questions and more.

Finding all the variables in an expression

It’s clear that the output value of f(x) depends on the input value

, and by comparison g(x) = 7 doesn’t. Some cases are tricky like h(x) = x − x, p(x) = 4x2 — (2x + 1)(2x − 1), or q(x) = sin(x)2 + cos(x)2 which also don’t depend on the value of x (can you see why?) but we won’t focus on these. Let’s ask a more general question: given an expression, which distinct variables appear in it? We can write a Python function distinct_variables which takes an expression (meaning any of our elements or combinators) and returns a Python set containing the variables.

If our expression is an element, like z or 7, the answer is clear. An expression which is only a variable contains one distinct variable, but an expression which is only a number contains no variables at all. We’d expect our function to behave accordingly:

>>> distinct_variables(Variable("z"))

{'z'}

The situation is more complicated when the expression is built up from some combinators, like y · z + x2. It’s easy for a human to read off all the variables, which are y, z, and x, but how do we extract these from the expression in Python? This is a Sum combinator, representing the sum of y · z and xz. The first expression in the sum has y and z, but the second has x and z. The sum contains all of the variables in these two expressions.

This suggests that we should use a recursive solution: the distinct_variables for a combinator are the collected distinct_variables for each of the expressions it contains. The end of the line are the variables and numbers, which obviously contain either one or zero variables. To implement the distinct_variables function, we need to handle the case of every element and combinator which is a valid expression.

def distinct_variables(exp):

if isinstance(exp, Variable):

return set(exp.symbol)

elif isinstance(exp, Number):

return set()

elif isinstance(exp, Sum):

return set().union(*[distinct_variables(exp) for exp in exp.exps])

elif isinstance(exp, Product):

return distinct_variables(exp.exp1).union(distinct_variables(exp.exp2))

elif isinstance(exp, Power):

return distinct_variables(exp.base).union(distinct_variables(exp.exponent))

elif isinstance(exp, Apply):

return distinct_variables(exp.argument)

else:

raise TypeError("Not a valid expression.")

As expected, our f_expression contains only the variable x:

>>> distinct_variables(f_expression)

{'x'}

If you’re familiar with the tree data structure, you’ll recognize this as a recursive traversal of the expression tree. By the time this function has completed running, it has called distinct_variables on every expression contained in the target expression, which are all of the nodes in the tree. That ensures that we’ve seen every variable, and we get the correct answers we expect. In the exercises at the end of the section, you can use a similar approach to find all of the numbers or all of the functions.

Evaluating an expression

Now we’ve got two representations of the same mathematical function f(x). One is the Python function, f, which is good for evaluating the function at a given input value of x. The new one is this tree data structure that describes the structure of the expression defining f(x). It turns out the latter representation has the best of both worlds — we can use it to evaluate f(x) as well, with only a little more work.

Mechanically, evaluating a function f(x) at, say, x = 5 means plugging in the value of 5 for x everywhere and then doing the arithmetic to find the result. If the expression were f(x) = x, plugging-in x = 5 tells us f(5) = 5. Another simple example is g(x) = 7, where plugging-in 5 in place of x has no effect — there are no appearances of x on the right-hand side, and the result of g(5) is 7.

The code to evaluate an expression in Python is similar to the code we wrote to find all variables. Instead of looking at the set of variables which appear in each sub-expression, we need to evaluate each sub-expression, then the combinators tell us how to combine these results to get the value of the whole expression.

The starting data we need are the values that need to be plugged-in and which variables they’ll replace. For instance an expression in two different variables like z(x,y) = 2xy3 need two values to get a result, for instance x = 3 and y = 2. In computer science terminology, these are called variable bindings. With these, we can evaluate the sub-expression y3 as (2)3 which equals 8. Another sub-expression is 2x, which evaluates to 2 · (3) = 6. These two are combined with the product combinator, and the value of the whole expression is the product of 6 and 8, or 48.

As we translate this procedure into Python code, I’m going to show you a slightly different style than in the previous example. Rather than having a separate “evaluate” function, we can add an evaluate method to each class representing an expression. To enforce this, we can create an abstract Expression base class with an abstract evaluate method, and have each kind of expression inherit from it. Here’s an Expression base class, complete with an evaluate method.

from abc import ABC, abstractmethod



class Expression(ABC):

@abstractmethod

def evaluate(self, **bindings):

pass

Because an expression may contain more than one variable, I set it up to allow you to pass in the variable bindings as keyword arguments. For instance, the bindings {x : 3, y : 2} mean “substitute 3 for x and 2 for y.” This gives us some nice syntactic sugar when evaluating an expression. If z represents the expression 2xy3 then once we’re done, we’ll be able to execute the following.

>>> z.evaluate(x=3,y=2)

48

This far, we’ve only got an abstract class. We need to do the work of having all of our expression classes inherit from Expression. For instance, a Number instance is an expression, as a number on its own, like “7,” is a valid expression. Regardless of the variable bindings provided, a number evaluates to itself.

class Number(Expression):

def __init__(self,number):

self.number = number

def evaluate(self, **bindings):

return self.number

For instance, evaluating Number(7).evaluate(x=3,y=6,q=-15) or any other evaluation for that matter, returns the underlying number: seven.

Handling variables is also simple. If we’re looking at the expression Variable(“x”), we only need to consult the bindings and see what number the variable “x” is set to. When we’re done, we should be able to run Variable(“x”).evaluate(x=5) and get 5 as a result. If we can’t find a binding for “x” then we can’t complete the evaluation and we need to raise an exception. Here’s the updated definition of the Variable class:

class Variable(Expression):

def __init__(self,symbol):

self.symbol = symbol

def evaluate(self, **bindings):

try:

return bindings[self.symbol]

except:

raise KeyError("Variable '{}' is not bound.".format(self.symbol))

With the elements handled, we need to turn our attention to the combinators. (Note that we won’t consider a Function object an Expression on its own, because a function like “sin” isn’t a standalone expression. It can only be evaluated when it’s given an argument, in the context of an Apply combinator.) For a combinator like Product, the rule used to evaluate it is simple: evaluate both expressions contained in the product, and then multiply the results together. No substitution needs to be performed in the product, but we’ll pass the bindings along to both sub-expressions in case either contains a Variable.

class Product(Expression):

def __init__(self, exp1, exp2):

self.exp1 = exp1

self.exp2 = exp2

def evaluate(self, **bindings):

return self.exp1.evaluate(**bindings) * self.exp2.evaluate(**bindings)

With these three classes updated with evaluate methods, we can now evaluate any expression built out of variables, numbers, and products. For instance:

>>> Product(Variable("x"), Variable("y")).evaluate(x=2,y=5)

10

Similarly, we can add an “evaluate” method to the Sum, Power, Difference, or Quotient combinators. Once we evaluate their sub-expressions, the name of the combinator tells us which operation we use to get the overall result. The “Apply” combinator works a bit differently, and it deserves some special attention. We need to dynamically look at a function name like “sin” or “sqrt” and figure out a recipe to compute its value. This can be done in a few ways, but I chose keeping a dictionary of known functions as data on the Apply class. As a first pass, we can make our evaluator aware of three named functions:

_function_bindings = {

"sin": math.sin,

"cos": math.cos,

"ln": math.log

}



class Apply(Expression):

def __init__(self,function,argument):

self.function = function

self.argument = argument

def evaluate(self, **bindings):

return _function_bindings[self.function.name](self.argument.evaluate(**bindings))

You can practice writing the rest of the “evaluate” methods yourself, or find them in the source code. Once you’ve got all of them fully implemented, you’ll be able to evaluate our f_expression.

>>> f_expression.evaluate(x=5)

-76.71394197305108

The result here isn’t important, only the fact that it’s the same as what the ordinary Python function f(x) gives us.

>>> f(x)

-76.71394197305108

Equipped with the evaluate function, our Expression objects can do the same work as their corresponding ordinary Python functions.

Expanding an expression

Many other things can be done with our expression data structures. In the exercises, you can try your hand building a few more Python functions which manipulate expressions in different ways. I’ll show you one more example for now, which I mentioned at the beginning of the article: expanding an expression. What I mean by this is taking any product or power of sums and carrying it out.

The relevant rule of algebra is the distributive property of sums and products. This rule says that a product of the form (a + b)c is equal to ac + bc and similarly that x(y + z) = xy + xz. For instance, our expression (3x2 + x)sin(x) is equal to 3x2sin(x) + xsin(x), which is called the expanded form of the first product. You can use this rule several times to expand more complicated expressions, for instance: