程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 面向組合子設計Coder,面向組合coder

面向組合子設計Coder,面向組合coder

編輯:C#入門知識

面向組合子設計Coder,面向組合coder


 

面向組合子

 

面向組合子(Combanitor-Oriented),是最近幫我打開新世界大門的一種pattern。緣起haskell,又見monad與ParseC,終於ajoo前輩的幾篇文章。

自去年9月起正式回歸C#以來,我又逐漸接受了不少新的paradigm(雖然主要原因還是在學校用C#的方法太山寨),其中對我影響比較深刻的就是codegen。此codegen非compiler中的codegen,可能更像是meta-programming中的codegen。抽象來說,就是作為一個嵌入於構建流程中的某一步驟,拿到一些元描述信息,來生成代碼。

我目前所接觸到的codegen的具體應用情景,有這樣幾種:
1.RPC相關的,數據打解包邏輯、Stub/Skeleton、組播等
2.配表轉代碼
3.策劃配出來的可視化行為樹轉代碼

從這些情景可以看出這種需求的典型特征:性能好、便於上層調用。

具體來說,我們還是拿這種形式跟一些比較傳統的形式做下對比:
RPC打解包邏輯直接自動走函數 V.S. protobuf
codegen成C#代碼的行為樹 V.S. 硬解腳本
C#結構描述的配置 V.S. 一坨meta二進制+一坨data二進制

又是一堆廢話,現在直接進入主題。

 

正文

 

首先定義一個概念,Coder,當然這跟平時一些低端討論串上經常引起的Coder還是Programmer中的Coder沒關系,這裡我們把它理解為一個函數,接收一個T描述結構作為參數,輸出一個字符串。

為了更C#一點,我們這樣定義Coder:

    public interface ICoder<in T>
    {
        string Code(T meta);
    }


這是所有Coder的基本表現形式,與之對應的,任何復雜的代碼生成程序,其實本質都是通過一個抽象數據結構生成一個字符串。

基於ICoder,我們先從最簡單的組合子開始構造,也就是"0"和"1":

    internal class UnitCoder<T> : ICoder<T>
    {
        readonly string output;
        public UnitCoder(string output)
        {
            this.output = output;
        }

        public override string Code(T meta)
        {
            return output;
        }
    }
    
    internal class ZeroCoder<T> : ICoder<T>
    {
        private static ZeroCoder<T> instance;
        public static ZeroCoder<T> Instance
        {
            get { return instance ?? (instance = new ZeroCoder<T>()); }
        }
        public override string Code(T meta)
        {
            return "";
        }
    }

 

UnitCoder:不論給什麼作為輸入,都只返回一個固定的字符串
ZeroCoder:不論給什麼作為輸入,都返回空字符串

只有這兩個的話,似乎還是什麼都不能做,我們需要一個最基本的可以讓我們定制的Coder:

    internal class BasicCoder<T> : ICoder<T>
    {
        private readonly Func<T, string> func;

        public BasicCoder(Func<T, string> func)
        {
            this.func = func;
        }

        public override string Code(T meta)
        {
            return func(meta);
        }
    }


假設現在有一個結構定義:

        class Meta1
        {
            public string Type;
            public string Name;
            public string Value;
        }


如此構造一個BasicCoder:

var basicCoder = Generator.GenBasic((Meta1 m) => string.Format(@"{0} {1} = {2}", m.Type, m.Name, m.Value));


這樣,通過給basicCoder傳不同的、具體的Meta1實例,這個Coder就跟真的Coder一樣coding出了不一樣的代碼。

僅有這三個還不夠,我們還需要想一種辦法將兩個Coder組合起來。說實話,這一塊代碼我寫得非常丑,整理成博客的原因也是希望有哪位前輩看到能指點一下。好了,直接上有很明顯bad smell的代碼。
首先需要對最基本的ICoder結構進行改造:

    public interface ICoder
    {
        string Code(object meta);
    }
    
    public interface ICoder<in T> : ICoder  
    {
        string Code(T meta);
    }


這樣ICoder來提供通用的Coder接口,方便後面的SequenceCoder。所有的Coder都復用一下這樣的邏輯:

    internal abstract class CoderBase<T> : ICoder<T>
    {
        private readonly T instance;

        public abstract string Code(T meta);

        public string Code(object meta)
        {
            if (meta is T)
            {
                return Code((T)meta);
            }

            throw new Exception("...");
        }
    }

 
然後我們著手實現SequenceCoder:

    internal class SequenceCoder<T> : CoderBase<T>
    {
        readonly ICoder[] coderArr;
        readonly Func<T, ICoder[], string> coderJoiner;

        public SequenceCoder(ICoder[] coderArr, Func<T, ICoder[], string> coderJoiner)
        {
            this.coderArr = coderArr;
            this.coderJoiner = coderJoiner;
        }

        public override string Code(T meta)
        {
            return coderJoiner(meta, coderArr);
        }
    }

 
我對SequenceCoder的定位是,Coder組合子系統內部的一個結合不同Coder的基礎組件。
有了SequenceCoder,我們就可以多出來很多有意義的東西了。

之前我們構造的basicCoder,是沒打出來語句末尾的";"的,我們來構造一下。先是前後綴的一些公共邏輯:

        internal static ICoder<T> WithPostfix<T>(this ICoder<T> coder, string postfix)
        {
            var coderPostfix = new UnitCoder<T>(postfix);

            return new SequenceCoder<T>(new ICoder[] { coder, coderPostfix }, (meta, arr) => string.Join("", coder.Code(meta), coderPostfix.Code(meta)));
        }
        internal static ICoder<T> WithPrefix<T>(this ICoder<T> coder, string prefix) where
        {
            var coderPrefix = new UnitCoder<T>(prefix);

            return new SequenceCoder<T>(new ICoder[] { coderPrefix, coder }, (meta, arr) => string.Join("", coderPrefix.Code(meta), coder.Code(meta)));
        }

 

然後是statementCoder:

var statementCoder = basicCoder.WithPostfix(";");


還可以被大括號包裹:

        public static ICoder<T> Brace<T>(this ICoder<T> coder)
        {
            return coder.WithPostfix("}").WithPrefix("{");
        }

 

var braceStatementCoder = statementCoder.Brace();

 
可以實現重復,也就是將一個ICoder<T>轉為一個ICoder<IEnumerable<T>>:

    internal class RepeatedCoder<T> : CoderBase<IEnumerable<T>>
    {
        private readonly ICoder coder;
        private readonly string seperator;
        private readonly Func<T, bool> predicate;
        public RepeatedCoder(ICoder<T> coder, string seperator, Func<T, bool> predicate)
        {
            this.coder = coder;
            this.seperator = seperator;
            this.predicate = predicate;
        }

        public override string Code(IEnumerable<T> meta)
        {
            bool first = true;
            return meta.Where(m=>predicate(m)).Select(m => coder.Code(m)).Aggregate("", (val, cur) =>
            {
                if (first)
                {
                    first = false;
                    return val + cur;
                }
                return val + seperator + cur;
            });
        }
    }


為了自己寫代碼方便,直接把seperator和predicate邏輯硬塞進去了,各位看官見諒。

構造一個重復Coder:

        public static ICoder<IEnumerable<T>> Many<T>(this ICoder<T> coder, string seperator) where T : class
        {
            return Generator.GenRepeated(coder, seperator);
        }

       

var repeatedCoder = basicCoder.WithPostfix(";").Many("\n");


這樣,給repeatedCoder一個Meta1的數組,他就會像一只coder一樣自動把每個元素轉成一行代碼。

有了這些還不夠,我們還是回歸需求本身。假設有這樣一個Coder :: ICoder<A>,這個Coder需要根據A的某個字段比如name寫出來一個 class name,需要根據另外一個比如IEnumerable<B>類型的字段寫出一系列field的定義。

我們期望生成的代碼形式:

class XXX
{    
    public t1 aaa = v1;
    public t2 bbb = v2;
}

 
假設A的結構定義是這樣的:

class A
{
    public string Name;
    public IEnumerable<Meta1> Fields;
}


其實這種需求也是我做出之前那種壞味代碼的原因,還是那句話,求高人指點!繼續上代碼,CombineCoder:

        public static ICoder<T> GenCombine<T, T1>(ICoder<T> tCoder, ICoder<T1> t1Coder, Func<T, T1> selector)
        {
            return new SequenceCoder<T>(new ICoder[] { tCoder, t1Coder },
                (meta, arr) =>
                    string.Format("{0}{1}", tCoder.Code(meta), t1Coder.Code(selector(meta))));
        }

 
復用我們之前構造的repeatedCoder

var coder1 = Generator.GenBasic((A a) => string.Format("class {0}", a.Name)).WithPostfix("\n");
var coder2 = repeatedCoder.Brace();


現在我們希望一個A->string的coder1與一個IEnumerable<Meta1>->string的coder2 combine起來,組合成一個A->string的classCoder,這樣做:

var classCoder = Generator.GenCombine(coder1, coder2, a => a.Fields);


好了大功告成,給classCoder一個A類型的元數據實例,就能輸出我們期望的字符串。

 

寫在最後

 

這篇博文的主體內容其實也差不多告一段落了。誠然,以上貼出的代碼不論是性能還是擴展性都存在很大的問題,但是前者對於一個codegen程序來說並不是關鍵考慮因素;而後者,正如之前所說,代碼的壞味還是存在不少,不僅在於SequenceCoder,也在於Combine,正因為這兩個目前的設計形式,導致了ICoder與ICoder<T>的壞味。

Sequence與Combine其實是相同的一種需求,如果將一個Coder看作一個monad的話,如何用一種可以理解的概念表示monad a與monad b的運算?我之前的確有嘗試過對bind進行生搬硬套,可是無論如何都不如目前實現的Combine方便,於是就產生了寫這篇小品文的念頭,期望高人解答。
因為是小品文,所以也沒像之前的消息隊列那篇一樣用了那麼多精力。本來2月份一直在看haskell和Parsec,打算寫一篇關於parsec跟行為樹的東西,結果後來因為一些事情擱置了。。只能之後再說了。

面向組合子的這種方式,除開我整篇文章提到的codegen,其實在游戲邏輯實現中還是不太常見的。我第一次見到是在我們工作室自研的行為樹引擎中,中間語言翻譯到特定語言(C#),用運行時庫中實現好的一些組合子組合起來成為一整棵行為樹。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved