面向组合子

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

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

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

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

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

面向组合子设计Coder的更多相关文章

  1. Java设计原则:面向接口的设计

    前言:在一个面向对象的系统中,系统的各种功能是由许许多多的不同对象协作完成的.在这种情况下,各个对象内部是如何实现对系统设计人员来说就不那么重要:而各个对象之间的协作关系则成为系统设计的关键.小到不同 ...

  2. JavaScript高级---组合模式设计

    一.设计模式 javascript里面给我们提供了很多种设计模式: 工厂.桥.组合.门面.适配器.装饰者.享元.代理.观察者.命令.责任链 在前面我们实现了工厂模式和桥模式 工厂模式 : 核心:为了生 ...

  3. 大到可以小说的Y组合子(零)

    问:啊!我想要一个匿名的递归… 答:Y(音同Why)… … … 问:作为一位命令式语言的使用者,为什么会突然折腾起Y组合子呢? 答:的确,这事儿要从很久以前的几次搁浅开始说起…上学的时候,从来没有接触 ...

  4. Y组合子

    Y组合子 Y组合子的用处 作者:王霄池链接:https://www.zhihu.com/question/21099081/answer/18830200来源:知乎著作权归作者所有.商业转载请联系作者 ...

  5. 进阶篇:3)面向制造的设计DFM

    本章目的:正确认识面向制造的设计-DFM. 1.DFM概念 DFM有两种描述是作者比较认可的. ①面向制造的设计(design for manufacturing简称DFM):指产品设计需要满足产品制 ...

  6. Lambda演算 - 简述Y组合子的作用

    Y组合子:\f.(\x.f(xx))(\x.f(xx)),接受一个函数,返回一个高阶函数 Y组合子用于生成匿名递归函数. 什么叫匿名递归函数,考虑以下C语言递归函数 int sum(int n) { ...

  7. 大到可以小说的Y组合子(一)

    问:上回乱扯淡了一通,这回该讲正题了吧. 答:OK. 先来列举一些我参考过,并从中受到启发的文章. (1.)老赵的一篇文章:使用Lambda表达式编写递归函数 (2.)装配脑袋的两篇文章:VS2008 ...

  8. 大到可以小说的Y组合子(三)

    答:关于Fix的问题你fix了吗? 问:慢着,让我想想,上次留下个什么问题来着?是说我们有了一个求不动点的函数Fix,但Fix却是显式递归的,是吧? 答:有劳你还记的这个问题. 问:Fix的参与背离了 ...

  9. 大到可以小说的Y组合子(二)

    问:上一回,你在最后曾提到"抽象性不足",这话怎么说? 答:试想,如果现在需要实现一个其它的递归(比如:Fibonacci),就必须把之前的模式从头套一遍,然后通过fib_make ...

随机推荐

  1. H5单页面手势滑屏切换原理

    H5单页面手势滑屏切换是采用HTML5 触摸事件(Touch) 和 CSS3动画(Transform,Transition)来实现的,效果图如下所示,本文简单说一下其实现原理和主要思路. 1.实现原理 ...

  2. lua 学习笔记(1)

    一.lua函数赋值与函数调用         在lua中函数名也是作为一种变量出现的,即函数和所有其他值一样都是匿名的,当要使用某个函数时,需要将该函数赋值给一个变量,这样在函数块的其他地方就可以通过 ...

  3. Angular企业级开发(2)-搭建Angular开发环境

    1.集成开发环境 个人或团队开发AngularJS项目时,有很多JavaScript编辑器可以选择.使用优秀的集成开发环境(Integrated Development Environment)能节省 ...

  4. 【开源】.Net Aop(静态织入)框架 BSF.Aop

    BSF.Aop .Net 免费开源,静态Aop织入(直接修改IL中间语言)框架,类似PostSharp(收费): 实现前后Aop切面和INotifyPropertyChanged注入方式. 开源地址: ...

  5. Javascript 代理模式模拟一个文件同步功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. 要想提高PHP的编程效率,你必须知道的要点

    1.当操作字符串并需要检验其长度是否满足某种要求时,你想当然地会使用strlen()函数.此函数执行起来相当快,因为它不做任何计算,只返回在zval 结构(C的内置数据结构,用于存储PHP变量)中存储 ...

  7. js面向对象学习 - 对象概念及创建对象

    原文地址:js面向对象学习笔记 一.对象概念 对象是什么?对象是“无序属性的集合,其属性可以包括基本值,对象或者函数”.也就是一组名值对的无序集合. 对象的特性(不可直接访问),也就是属性包含两种,数 ...

  8. WCF基础

    初入职场,开始接触C#,开始接触WCF,那么从头开始学习吧,边学边补充. SOA Service-Oriented Architecture,面向服务架构,粗粒度.开放式.松耦合的服务结构,将应用程序 ...

  9. 清除打印机队列中无法清除的任务 & 清空打印池

    故障现象典型表现为以下两种情况 1.当打印任务开始进行时,这些打印任务便被保存在打印作业列表(也称打印队列)内.如果打印机因意外暂停(如打印机未连接)而未完成打印任务,则该打印任务将列入打印队列,并且 ...

  10. 【原】npm 常用命令详解

    今年上半年在学习gulp的使用,对npm的掌握是必不可少的,经常到npm官网查询文档让我感到不爽,还不如整理了一些常用的命令到自己博客上,于是根据自己的理解简单翻译过来,终于有点输出,想学习npm这块 ...