符号化表达式设计(一)
要实现一个类似于matlab可以计算表达式的程序,
例如:
x = agauss(4, 0.3, 1) /* agauss(u, s, d) 表示产生类似于高斯分布的随机数,u表示平均值,s表示方差sigma,d表示允许的最大偏离值。 */
y = x^2 - x;
print eval(y) /* eval(x) 表示对x进行求值 */
与一般的计算器不一样,求值不是实时计算,而是先用符号表示,类似于包含未知变量,然后给定未知变量的值,对符号表达式进行计算。
程序设计
表达式可以用一个类似于二叉树的结构组织起来,比如 a + b, 根节点 +, 包含左右两个节点a, b作为操作数,而agauss这类特殊的函数,则可以包含一个节点数组作为参数。
所以最基本的表达式节点,就是数值节点,
class ExprNode { protected: double m_value; public: ExprNode(double v=0):m_value(v){} virtual double Value() { return m_value; } void Value(double t) { m_value = t; } virtual void Evaluate() {} };
然后就是变量节点,变量会有一个名字, 然后会有一个数值或者表达式来表示它的值,因为数值也作为表达式节点,所以与表达式作为值的情况是统一的,定义如下,
class ExprVariableNode : public ExprNode { private: string m_name; ExprNode* m_expr; public: ExprVariableNode(string nm, ExprNode* e):ExprNode(), m_name(nm), m_expr(e){} virtual void Evaluate() { m_value = m_expr->Value(); } };
这里或许存在这样一个问题,对变量节点进行求值的时候,使用的是m_expr->Value(), 而不是 m_expr->Evalaue(), 这是因为我想避免嵌套求值。对于一开始给的例子,
y = x^2 - x; 如果使用前套求值, 那么 x->Evaluate()会调用两次,导致同一个式子里面x的值不同。如果要避免多次调用,就需要有个标志来表明它已经求过值,在调用Evaluate之后将其置为true,
然后在需要更新的时候将标志置为false。如果不使用标识符,我们可以在创建表达式节点的时候,将所有节点放到一个列表里面,列表里面的节点顺序会对应到求值顺序上,那么需要求值的时候,遍历列表,
对每个节点调用Evaluate,不需要进行嵌套的调用。所以是需要一个列表来保存所有创建的节点的。
除了这两个简单的节点,表达式必然需要支持四则运算等常用的操作或者函数。为每一个操作或函数创建一个节点类显然是很浪费的。考虑到四则运算都是左右两个操作数,可以将这一类节点定义如下,
class ExprOpNode : public ExprNode { protected: ExprNode* m_pleft; ExprNode* m_pright; function<double(double)> m_op; public: ExprOpNode(ExprNode* l, ExprNode* r, function<double(double)> op) : ExprNode(), m_pleft(l), m_pright(r), m_op(op){} virtual void Evaluate() { m_value = m_op(m_pleft->Value(), m_pright->Value());} };
在创建"+"节点的时候,将 plus<double>()传入作为op参数就可以了,那么四则运算就可以支持了。进一步,可以在ExprOpNode的基础上进一步封装,使得创建节点的时候不需要处理op参数,例如,
class ExprPlusNode : public ExprOpNode { public: ExprPlusNode(ExprNode* l,ExprNode* r) : ExprOpNode(l, r, plus<double(double)>()){} };
对于agauss函数,使用类似于ExprOpNode的方式,则可以创建下面的节点,
template<typename FuncOp> class ExprFuncNode : public ExprNode { protected: FuncOp m_func; vector<ExprNode*> m_params; public: ExprFuncNode(ExprNode** params, unsigned int size): ExprNode() { Param(params,size); } ExprFuncNode():ExprNode(){} vector<ExprNode*>& Param() { return m_params;} void Param(ExprNode** params, unsigned int size) { m_params = vector<ExprNode*>(params, params + size); } void AddParam(ExprNode* p) { m_params.push_back(p);} virtual void Evaluate() { unsigned int N = m_params.size(); vector<ExprValueType> t_params(N); for(unsigned int i=0; i<N;i++) { t_params[i] = m_params[i]->Value(); } m_value = m_func(t_params); } };
然后定义AGauss的仿函数,
class ExprFuncAGauss { private: const static int size = 3; typedef std::normal_distribution<> Dis; typedef std::mt19937 Gen; std::random_device rd; public: double operator() (vector<double> params) { assert(size==params.size()); assert(params[2]>0); Gen gen(rd()); Dis dis(params[0], params[1]); double result = 0; do { result = dis(gen); }while(abs(result-params[0])<=params[2]); return result; } };
由于ExprFuncNode是使用模板定义的,所以定义ExprAGaussNode比定义ExprPlusNode简单一些,直接使用typedef定义即可,如下,
typedef ExprFuncNode<ExprFuncAGauss> ExprAGaussNode;
除了使用上面的方式,我们也可以直接定义ExprAGaussNode, 如下,
class ExprAGaussNode : public ExprNode { private: ExprNode* m_mu; ExprNode* m_sigma; ExprNode* m_dlimit; typedef std::normal_distribution<> Dis; typedef std::mt19937 Gen; std::random_device rd; public: ExprAGaussNode(ExprNode* m, ExprNode* s, ExprNode* d) : m_mu(m), m_sigma(s), m_dlimit(d){} virtual void Evaluate() { Gen gen(rd()); Dis dis(params[0], params[1]); double result = 0; do { result = dis(gen); }while(abs(result-params[0])<=params[2]); m_value = result; } };
使用模板的方式,应该是更方便一些的,可以直接扩展到其它的,带有多个参数的自定义函数上,对每个自定义函数,只需要定义函数实现的仿函数就可以了。
很可惜的是,使用vector<ExprNode*> m_params可以应对任意参数个数的函数,但只能用于自定义的函数。对于cmath里面的其它简单函数,比如sin,就不能这样用了。
并且,不能像plus函数一样,作为构造函数的参数传入,也不能像agauss函数一样,作为模板参数传入。而cmath中还有很多像sin这样的函数。
我们来比较一下plus和sin函数的实现,如下,
// header: <functional> template< class T > struct plus { T operator()(const T &lhs, const T &rhs) const { return lhs + rhs; } } // header: <cmath> double sin( double arg ); template< class T > complex<T> sin( const complex<T>& z ); template< class T > valarray<T> sin( const valarray<T>& va );
在仿函数的头文件里面,实现了plus的仿函数结构。而在cmath头文件里,sin使用模板函数实现多种参数类型的支持。
所以plus可以作为以类型作为模板参数,也可以以仿函数作为参数。而sin则不可以,但是sin可以作为函数指针的参数。
要实现对sin这一类简单函数的支持,显然使用模板是最方便的方法,这就需要将sin函数转化为仿函数的结构。参考http://stackoverflow.com/questions/10213427/passing-a-functor-as-c-template-parameter ,我们就有了这样的转换方法,
template< double (*FuncPtr)(double) > struct FuncToType { double operator()(double t) { return FuncPtr(t); } };
然后我们定义支持一个参数的函数节点的模板,
template<typename FuncName> class ExprFuncOneNode : public ExprNode { private: ExprNode* m_pexpr; FuncName m_func; public: ExprFuncOneNode(ExprNode* e):m_pexor(e){} virtual void Evaluate() { m_value = m_func(m_pexpr->Value());} };
现在再来看四则运算符节点的实现,或许改成使用模板方式实现更好。所以对函数节点的实现,就可以按参数个数来分类,分别使用模板实现。
好,表达式的数据结构设计就到此为止。如果您有更好的建议,或者上面的代码或思路有问题,请多多指教。