zoukankan      html  css  js  c++  java
  • 面向组合子设计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("
    ");


    这样,给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("
    ");
    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#),用运行时库中实现好的一些组合子组合起来成为一整棵行为树。

    最后又做了些改进,有机会还会写续篇对扩展部分的一些思路进行介绍。

    最新的代码放在了github上:CodeC

    同时,这篇关于游戏服务端的文章中提到的RPC和数据模型相关的代码自动生成器,也是基于Coder组合子写成。

    最终的代码生成器在这里:Phial.CodeGenerator

  • 相关阅读:
    MVC思想-程序的控制流程-Struts2和SpringMVC黑马流程图
    代理模式
    显卡
    感悟:Java新手一点想法
    java企业级开发的实质就是前台后台如何交互的-各个对象之间如何交互,通信的-程序执行的流程是怎样的
    $.ajax()方法详解--极快瑞中的阿贾克斯函数
    初学者必读之AJAX简单实例2
    初学者必读原生AJAX-异步的javaScript和XML
    c#输入方法名来调用方法(反射)
    unity接入讯飞教程
  • 原文地址:https://www.cnblogs.com/fingerpass/p/combinator-oriented-designing.html
Copyright © 2011-2022 走看看