要實現一個類似於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進行求值 */
與一般的計算機不一樣,求值不是Realtime Compute,而是先用符號表示,類似於包含未知變數,然後給定未知變數的值,對符號運算式進行計算。
程式設計
運算式可以用一個類似於二叉樹的結構組織起來,比如 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());}};
現在再來看四則運算子節點的實現,或許改成使用模板方式實現更好。所以對函數節點的實現,就可以按參數個數來分類,分別使用模板實現。
好,運算式的資料結構設計就到此為止。如果您有更好的建議,或者上面的代碼或思路有問題,請多多指教。