If you haven’t done so yet, read parts one and two first or watch the video that inspired these posts.
You can find a notebook with the state after this blog post here.
Last time there was a problem in the expression simplification left, which is apparent when looking at these two expressions:
str(Mul(Con(2), Mul(Con(3), Var('x'))).simplify()) # returns '2*3*x'
str(Mul(Mul(Con(2), Con(3)), Var('x')).simplify()) # returns '6*x'
They model the same expression, so both should return the second (simpler) string. The actual solution to this will come later in the post, but first I noticed another problem:
Proper Operation Modelling
The way we model subtraction and division at the moment is not really correct.
They are currently binary operations on the same binding level as addition and
multiplication respectively. In reality a - b
is more of shorthand for
a + (-b)
(similarly for division and the inverse) and that’s the way I want
to model it now.
Let’s start with the simpler of the two cases and replace Sub()
:
Representation for Debugging purposes
For some debugging that I had to do, I first implemented the __repr__
function in all base classes. This allows me to call repr(expr)
on any
expression and get a string that would work as a constructor for the
expression. The implementation is relatively straight forward:
class SinExpr(Expr):
# ...
def __repr__(self):
return str("{t}({a})".format(
t=self.__class__.__name__,
a=str(self),
))
class BinExpr(Expr):
# ...
def __repr__(self):
return str("{t}({a}, {b})".format(
t=self.__class__.__name__,
a=repr(self.a),b=repr(self.b),
))
And of cause adding a small test:
assert repr(expr) == 'Add(Mul(Con(3), Pow(Var(x), Con(2))), Sub(Var(y), Div(Con(6), Con(3))))'
Replacement of Sub()
Ok, now to the replacement of Sub()
. First we add a new base class for
operations with just one argument, the unary operations:
class UnaExpr(Expr):
def __init__(self, a):
self.a = a
def __repr__(self):
return str("{t}({a})".format(
t=self.__class__.__name__,
a=repr(self.a),
))
@property
def _bindingLevel(self):
return 4
These get the binding level 4, consequently I shifted the binding level of the
singular expressions to 5. Hard coding these values is not really a good idea
though, and in C++
I would use an enum
instead. I’ll have to think about
how to do this in a better way later. But first I implemented the negation class
Neg()
:
class Neg(UnaExpr):
def __str__(self):
return '-{sub}'.format(sub=self.a)
def eval(self, env):
return -self.a.eval(env)
def partial(self, var):
return Neg(self.a.partial(var))
@property
def constValue(self):
self.cv_a = self.a.constValue
if self.cv_a:
return -self.cv_a
return None
def simplify(self):
self.a = self.a.simplify()
if self.a.constValue == 0:
return Con(0)
return self
This probably does not need to be discussed in detail.
The class Sub
gets deleted now and is replaced by a function of the same
name, that mimics the behaviour of the previous constructor of Sub
:
def Sub(expr_a, expr_b):
return Add(expr_a, Neg(expr_b))
You might notice, that some of the previous simplification logic got lost on
the way. This actually already caught by a more general rule in Add
:
class Add(BinExpr):
# ...
def _selfSimplify(self):
# if one child is zero, return the other one
if self.cv_a == 0:
return self.b
if self.cv_b == 0:
return self.a
return self
There’s one other change to Add
that we need:
class Add(BinExpr):
# ...
@property
def _strOperator(self):
if isinstance(self.b, Neg):
return "" # Neg will produce the minus sign
return "+"
Ok, great. If we now run all our test again, we see should only need to change
the __repr__
test to:
assert repr(expr) == 'Add(Mul(Con(3), Pow(Var(x), Con(2))), Add(Var(y), Neg(Div(Con(6), Con(3)))))'
Everything else should be passing fine.
Replacement of Div()
We now do the same thing for division, deleting the class Div
and
introducing:
class Inv(UnaExpr):
def __str__(self):
return '1/{sub}'.format(sub=self.a)
def eval(self, env):
return 1/self.a.eval(env)
def partial(self, var):
return Neg(Mul(
self.a.partial(var),
Inv(Pow(self.a, Con(2)))
))
@property
def constValue(self):
self.cv_a = self.a.constValue
if self.cv_a:
return 1/self.cv_a
return None
def simplify(self):
self.a = self.a.simplify()
if isinstance(self.a, Inv):
return self.a.a
if self.a.constValue == 0:
raise ValueError("Division by zero")
return self
def Div(expr_a, expr_b):
return Mul(expr_a, Inv(expr_b))
The arguments are very analogous to the Sub
case, so I’m not repeating them
again.
Running the tests, I needed to change the simple str()
and repr()
tests to:
assert str(expr) == '3*x^2+y-6*1/3'
assert repr(expr) == 'Add(Mul(Con(3), Pow(Var(x), Con(2))), Add(Var(y), Neg(Mul(Con(6), Inv(Con(3))))))'
And I deleted the other “simple” str()
tests, since I really just care about
the simplified string representation. All the tests there stayed valid.
Advanced Expression simplification
Now back to the original problem:
str(Mul(Con(2), Mul(Con(3), Var('x'))).simplify()) # returns '2*3*x'
str(Mul(Mul(Con(2), Con(3)), Var('x')).simplify()) # returns '6*x'
Both lines model the same expression, so both should return the second (simpler) string. I think, I will make another separate post about this.