符號化運算式設計(一)

來源:互聯網
上載者:User

要實現一個類似於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());}};

現在再來看四則運算子節點的實現,或許改成使用模板方式實現更好。所以對函數節點的實現,就可以按參數個數來分類,分別使用模板實現。

好,運算式的資料結構設計就到此為止。如果您有更好的建議,或者上面的代碼或思路有問題,請多多指教。

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.