/*
Expression Trees, "New CSC123 Approach", Replacing the Visitor Pattern.
Adding an element of Functional programming.
Functional types A->B are expressed as Func. Types
A*B->C are Func, and Curried functions A->B->C are
Func>. Functions that do not return values (A->void)
are of type Action.
Lambda terms are written x => y => x (lambda x.lambda y.x), which has
type Func>.
*/
using System;
public interface expr // abstract interface for all expression trees
{
void print(); // can combine with OOP approach
// match is generic over the return type T
T match(Func intf,
Func plusf,
Func timesf,
Func divf,
Func negf
);
}//expr
public class intnode : expr // integer expreesion, or leave nodes
{
public int val;
public intnode(int v) {val = v; }
public void print() { Console.Write(val); }
public T match(Func intf,
Func plusf,
Func timesf,
Func divf,
Func negf
) { return intf(this); } // calls the correct function on this
} // intnode
public class operatornode // interior node superclass for code sharing
{
public expr left, right; // type of subtrees is "expr", not "node"!
public operatornode(expr l, expr r) { left=l; right=r; }
protected void print(String op)
{
Console.Write("("+op+" "); // prints in prefix notation "(+ 3 4)"
left.print(); Console.Write(" ");
right.print();
Console.Write(")");
}
} //operatornode superclass
public class plusnode : operatornode, expr // + operator expression
{
public plusnode(expr l, expr r) : base(l,r) {}
public void print() { base.print("+"); }
public T match(Func intf,
Func plusf,
Func timesf,
Func divf,
Func negf
) => plusf(this); // same as {return plusf(this);}
} // plusnode
public class timesnode : operatornode, expr // * operator expression
{
public timesnode(expr l, expr r) : base(l,r) {}
public void print() { print("*"); } //"base" is not needed (overloading)
public T match(Func intf,
Func plusf,
Func timesf,
Func divf,
Func negf
) => timesf(this);
} // timesnode
public class divnode : operatornode, expr // * operator expression
{
public divnode(expr l, expr r) : base(l,r) {}
public void print() { print("/"); }
public T match(Func intf,
Func plusf,
Func timesf,
Func divf,
Func negf
) => divf(this);
} // timesnode
public class negnode : expr { // unary minus
public expr subexpr; // the expression being negated
public negnode(expr s) { subexpr=s; }
public void print() {
Console.Write("-("); subexpr.print(); Console.Write(")");
}//print
public T match(Func intf,
Func plusf,
Func timesf,
Func divf,
Func negf
) => negf(this);
}//negnode
/////////////////////////////////////////////////
public static class exprops { // operations on exprs
public static int eval(expr e) {
return e.match(
intf: n => n.val,
plusf: n => eval(n.left) + eval(n.right),
timesf: n => eval(n.left) * eval(n.right),
negf: n => -1 * eval(n.subexpr),
divf: n => {
int d = eval(n.right);
if (d==0) throw new Exception("division by zero");
else return eval(n.left) / d;
}
); // e.match
}//eval
public static int size(expr e) { // number of nodes
return e.match_op(
Int: n => 1,
Binop: n => size(n.left) + size(n.right) + 1,
Uniop: n => size(n.subexpr) + 1
);
}//size
public static string to_string(expr e) { //infix printing, functional style
return e.match(
intf: n=> ""+n.val,
plusf: n => "(" + to_string(n.left) + "+" + to_string(n.right)+ ")",
timesf: n => "(" + to_string(n.left) + "*" + to_string(n.right)+ ")",
divf: n => "(" + to_string(n.left) + "/" + to_string(n.right)+ ")",
negf: n => // avoid () around integers: -3 instead of -(3)
n.subexpr.match( // secondary match (not genuine pattern matching)
intf: s => "-" + s.val,
negf: s => to_string(s.subexpr), // --3 cancels out to 3
plusf: s => "-" + to_string(n.subexpr),
timesf: s => "-" + to_string(n.subexpr),
divf: s => "-" + to_string(n.subexpr)
)//submatch
);
}//to_string
// version of match that does not return a value
public static void match_do(this expr e, // "extension method"
Action Int,
Action Plus,
Action Times,
Action Div,
Action Neg
) {
e.match(n=> {Int(n); return 0;},
n=> {Plus(n); return 0; },
n=> {Times(n); return 0; },
n=> {Div(n); return 0; },
n=> {Neg(n); return 0; }
);
}//match_do
// version of match that unifies all binary operator nodes in
// one clause:
public static T match_op(this expr e,
Func Int,
Func Binop,
Func Uniop) =>
e.match(intf: Int,
plusf: n => Binop(n),
timesf: n => Binop(n),
divf: n => Binop(n),
negf: Uniop);
}// exprops
public class exptree3b
{
public static void println(T x) {Console.WriteLine(x);}
public static void Main()
{
expr A = new plusnode(new timesnode(new intnode(2),new intnode(3)),
new plusnode(new intnode(1),new intnode(4)));
A.print();
Console.WriteLine(" evaluates to " + exprops.eval(A));
Console.WriteLine(" size " + exprops.size(A));
Console.WriteLine(" prefix form: " + exprops.to_string(A));
expr B = new plusnode(new intnode(4),new negnode(new negnode(new intnode(2))));
string s = exprops.to_string(B);
Console.WriteLine("B in infix form: " + s);
} // main
}
/*
Notice that the functions inside exprops class, which was just created for
organization, are all static. Dynamic dispatch occurs only once with
each call to .match, which invokes the correct function on the expression
type being called on. This elminates the double-dispatch complexity of the
visitor pattern and it's convoluted syntax (v.accept(visitor)).
We can also provide variants on match, such as match_do, which
perform an Action on each node without returning a value.
The downside of this approach, however, is that we lose the ability to
easily add another type of node to design. We would have to change
the match signature, along with how it's implemented in every single
expr subclass to accommodate the new node. But we are able to add new
operations on exisiting nodes quite easily. This is a general
trade-off between object oriented programming and functional
programmming. With the latter, the type you want to define your
functions on have to be fixed. If the type definition changes (by
adding a "modnode" for the % operator, for example), then all existing
functions become invalid. We can't just implement a function
piecemeal. In OOP, it's the *interface* of the objects that must be
fixed. One can add new objects that implement the interface but it's
difficult to change the interface without changing each object.
Functional programming allows us to add new functions easily, while
OOP allows us to add new kinds of objects easily.
*/