BT

您是否属于早期采用者或者创新人士?InfoQ正在努力为您设计更多新功能。了解更多

用Erlang实现领域特定语言

| 作者 Dennis Byrne 关注 0 他的粉丝 ,译者 郭晓刚 关注 0 他的粉丝 发布于 2008年11月25日. 估计阅读时间: 25 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

人们对Erlang谈得很多,但话题往往集中在并发方面,很少涉及Erlang平台的其他强大特性。本文正是打算讨论其中一项没有得到足够重视的特性—— Erlang是打造领域特定语言的极佳平台。我在这里选择了投资金融领域作为例子,向你展示在Erlang运行时系统里,翻译并执行平直的英文语句是多么简单的一件事情。你顺带还会学到一星半点函数式编程的知识。如果在学习当中有什么不明白的,Erlang参考手册是你的好帮手。

我们首先高屋建瓴地看一下这种DSL的使用情况,然后再一步步详细讨论它的实现。

 $ # 首先定义业务规则
 $ echo "buy 9000 shares of GOOG when price is less than 500" > biz_rules.txt
 $ echo "sell 400 shares of MSFT when price is greater than 30" >> biz_rules.txt
 $ echo "buy 7000 shares of AAPL when price is less than 160" >> biz_rules.txt
 $ erl      # 启动Erlang仿真器(类似于irb或者beanshell)
 1> c(dsl). % compile and load, assumes dsl.erl is in the current directory
 2> Pid = spawn(fun() -> dsl:broker() end). % start a broker in parallel
 3> Functions = dsl:load_biz_rules(Pid, "biz_rules.txt").
 4> MarketData = [{'GOOG', 498}, {'MSFT', 30}, {'AAPL', 158}].
 5> dsl:apply_biz_rules(Functions, MarketData).
 Order placed: buying 9000 shares of 'GOOG'
 Order placed: buying 7000 shares of 'AAPL'

实现

前三行echo命令创建biz_rules.txt文件,并向其中写入三条规则。这些规则的逻辑很直白;其表达形式与直接从用户口中说出来的话相差无几。

 buy 9000 shares of GOOG when price is less than 500
 sell 400 shares of MSFT when price is greater than 30
 buy 7000 shares of AAPL when price is less than 160

我们的DSL放在一个名为“dsl”的Erlang模块中,模块只有一个文件dsl.erl。在erl代码中,第一条命令是用内建的c函数编译并加载dsl模块。

 1>c(dsl). % compiles and loads, assumes dsl.erl is in the current directory

产生一个Broker

dsl模块有个公共函数名为broker(译注:股票经纪人)。

 broker() ->
     receive
       {buy, Quantity, Ticker} ->
          % 向外部系统下单的具体代码放在这里
%
Msg = "Order placed: buying ~p shares of ~p", io:format(Msg, [Quantity, Ticker]), broker(); {sell, Quantity, Ticker} -> % 向外部系统下单的具体代码放在这里
%
Msg = "Order placed: selling ~p shares of ~p", io:format(Msg, [Quantity, Ticker]), broker() end.

broker函数等待消息并反复调用receive块。它只接收两类消息:卖出股票的消息和买入股票的消息。由于这里只是做一个演示,所以具体的下单操作就省略了。

请注意broker是尾递归的。在命令型(imperative)编程语言里,一般会用循环来达到相同目的。而在Erlang中不需要使用循环,因为尾递归函数会被自动优化在固定的空间中运行。Erlang开发者不需要承担手工管理内存的责职,可以远离“for”、“while”、“do”这些关键字。这三个关键字之于栈,就像是“malloc”和“dealloc”之于堆……多余。

给仿真器的第二行命令是产生一个匿名函数的Erlang进程,并且返回进程ID。进程ID的值绑定到变量Pid。

 2> Pid = spawn(fun() -> dsl:broker() end). % call broker in parallel

在Erlang里,匿名函数以 “fun” 关键字起头,“end” 关键字结尾。看到这些字眼的时候请留心,因为本文中将出现非常多的匿名函数。上面的这个匿名函数很简单,它仅仅包装了一下broker。

我们不打算深入讲解Erlang的众多有趣特性。暂且请将上述代码看作是产生了一条单独的执行路径,下文将称之为broker进程。内建的spawn函数返回了一个进程ID,我们将通过这个进程ID向该进程发出买卖单。

加载业务规则的第一种途径

接下来我们调用load_biz_rules,它是dsl模块的另一个公开函数。

 3> Functions = dsl:load_biz_rules(Pid, "biz_rules.txt").

传给load_biz_rules的参数是broker进程的ID和业务规则所在的文件;返回的是一个Erlang函数列表。本文将出现许多返回函数的函数,不熟悉函数式编程的人可能觉得理解起来有些障碍。请联想一下在面向对象的世界里,一个对象创建另一个对象并通过方法返回对象,是很寻常的事情——甚至还有专门针对该情形的设计模式,比如抽象工厂模式。

load_biz_rules所返回的列表中每一个函数元素,都代表着先前写入到biz_rules.txt的一条业务规则。在面向对象编程语言里,我们大概会用一组对象实例来给这些规则建模;而在Erlang里,我们用函数。

 load_biz_rules(Pid, File) ->
     {ok, Bin} = file:read_file(File),
     Rules = string:tokens(erlang:binary_to_list(Bin), "\n"),
     [rule_to_function(Pid, Rule) || Rule <- Rules].

load_biz_rules函数首先将文件读入内存。文件的内容被分割放入一个字符串列表,然后绑定到名为Rules的变量。在Erlang中,函数的最后一行默认成为函数的返回值(和Ruby一样),并且总是以句号结尾。load_biz_rules的最后一行执行了一次列表推导(list comprehension),构建并返回一个函数列表。

熟悉列表推导的读者会知道Rule >- Rules部分是发生器(generator),而rule_to_function(Pid, Rule)部分是表达式模板(expression template)。这是什么意思?它的意思是我们创建一个新的列表,然后用Rules列表里的元素经过变换之后填充到新列表。 rule_to_function函数完成实际的变换工作。换句话说,最后一行的意思是“把broker进程的ID和Rules中的每一条Rule传递给 rule_to_function……然后把rule_to_function返回的函数列一个表给我”。

 rule_to_function(Pid, Rule) ->
     {ok, Scanned, _} = erl_scan:string(Rule),
     [{_,_,Action},{_,_,Quantity},_,_|Tail] = Scanned,
     [{_,_,Ticker},_,_,_,{_,_,Operator},_,{_,_,Limit}] = Tail,
     to_function(Pid, Action, Quantity, Ticker, Operator, Limit).

第一行代码将传给rule_to_function的Rule字符串扫描进一个元组(tuple)。接下来两行用模式匹配摘出实施规则所必需的数据。摘出来的值被绑定到Quantity、Ticker(译注:股票代码)等变量。接着broker进程的ID和摘出的5个值被传给to_function函数。 to_function将构建并返回代表着一条业务规则的函数。

我们将讨论to_function的两种实现,首先看一个比较偏向实用的版本。

 to_function(Pid, Action, Quantity, Ticker, Operator, Limit) ->
     fun(Ticker_, Price) ->
        if
          Ticker =:= Ticker_ andalso
          ( ( Price < Limit andalso Operator =:= less ) orelse 
            ( Price > Limit andalso Operator =:= greater ) ) ->
              Pid ! {Action, Quantity, Ticker}; % place an order 
          true ->
              erlang:display("no rule applied")
        end
     end.

这个to_function实现做了一件事——返回一个匿名函数。匿名函数的参数是两项市场数据:股票代码和价格。传给该函数的股票代码和价格,将与业务规则中指定的股票代码及价格相比较。如果匹配上了,就用发送操作符(即!符号)向broker进程发送一则消息,告诉它下单。

加载业务规则的第二种途径

to_function的第二种实现学院味比较浓一些。它构造一种抽象形式的Erlang表达式,并返回一个匿名函数,让它以后再动态地求解。

 to_function(Pid, Action, Quantity, Ticker, Operator, Limit) ->
     Abstract = rule_to_abstract(Action, Quantity, Ticker, Operator, Limit),
     fun(Ticker_, Price) ->
     	TickerBinding = erl_eval:add_binding('Ticker', Ticker_, erl_eval:new_bindings()),
       PriceBindings = erl_eval:add_binding('Price', Price, TickerBinding),
 	Bindings = erl_eval:add_binding('Pid', Pid, PriceBindings),
       erl_eval:exprs(Abstract, Bindings)
     end.

函数的第一行将脏活都委托给了rule_to_abstract函数。你不应该花太多时间研究rule_to_abstract,除非你是觉得Perl很对胃口的那种人。

 rule_to_abstract(Action, Quantity, Ticker, Operator, Limit) ->
     Comparison = if Operator =:= greater -> '>'; true -> '<' end,
     [{'if',1,
       [{clause,1,[],
         [[{op,1,
             'andalso',
        	    {op,1,'=:=',{atom,1,Ticker},{var,1,'Ticker'}},
        	    {op,1,Comparison,{var,1,'Price'},{integer,1,Limit}}}]],
  		[{op,1,
             '!',
             {var,1,'Pid'},
             {tuple,1,[{atom,1,Action},
                       {integer,1,Quantity},
                       {atom,1,Ticker}]}}]},
        {clause,1,[],
         [[{atom,1,true}]],
         [{call,1,
             {remote,1,{atom,1,erlang},{atom,1,display}},
             [{string,1,"no rule applied"}]}]}]}].

rule_to_abstract函数构造并返回一个抽象形式的Erlang控制结构。这个控制结构是由一系列具体限制条件组成的一个“if”语句。顺便一提,上面的抽象形式用了后缀运算符的写法,不同于一般Erlang语法的中缀写法。如果把它套用到单条规则上面,我们实际上是用编程的方式构建以下代码的抽象形式:

      	 if
 	           Ticker =:= ‘GOOG’ andalso Price < 500 ->
 	        	Pid ! {sell, 9000, ‘GOOG’}; % place order with broker
 	           true ->
 	              erlang:display("no rule applied")
 	    end

这个版本的to_function取得业务逻辑的抽象形式之后,会返回一个匿名函数(与前一个版本的to_function一样)。在匿名函数里,股票代码、价格条件、broker进程ID这三个变量被从执行作用域中抓出来,经由内建函数erl_eval:add_binding动态绑定到构建出来的表达式中。最后由内建的erl_eval:exprs库函数执行构建好的表达式。

应用业务规则

现在我们加载了业务规则,也为每条规则构造好了Erlang函数,是时候把市场数据参数传给它们了。市场数据用一个元组列表来表示。每个元组代表一对股票代码和股价。

 4> MarketData = [{'GOOG', 498}, {'MSFT', 30}, {'AAPL', 158}].
 5> dsl:apply_biz_rules(Functions, MarketData).

把函数形式的业务规则,以及市场数据交给apply_biz_rules函数。

apply_biz_rules(Functions, MarketData) ->
 	lists:map(fun({Ticker,Price}) ->
               	lists:map(fun(Function) ->
                                 Function(Ticker, Price)
                           end, Functions)
                 end, MarketData).

幸亏我们只有三条业务规则,因为apply_biz_rules的运行时是指数增长的。不熟悉Erlang语法或者不留心听讲的话,上面的算法读起来有点难懂。apply_biz_rules函数将一个内部函数映射到市场数据中的每一对股票代码/股价。内部函数又将第二个内部函数映射到每一个业务规则函数。第二个内部函数再将股票代码和股价传递给业务规则函数!

执行apply_biz_rules,broker进程确认它收到买入9000股Google和7000股Apple的指令。

 5> dsl:apply_biz_rules(Functions, MarketData).
 Order placed: buying 9000 shares of 'GOOG'
 Order placed: buying 7000 shares of 'AAPL'

执行结果中没有买入或卖出Microsoft的股票。回头检查一下业务规则,对比一下市场数据可以确定程序的行为是正确的。

 buy 9000 shares of GOOG when price is less than 500
 sell 400 shares of MSFT when price is greater than 30
 buy 7000 shares of AAPL when price is less than 160

 4> MarketData = [{'GOOG', 498}, {'MSFT', 30}, {'AAPL', 158}].

如果Google的股价涨了7块,同时我们改变了卖出Microsoft的条件,那么将会观察到不同的结果。

 sell 400 shares of MSFT when price is greater than 27

 6> UpdatedFunctions = dsl:load_biz_rules(Pid, "new_biz_rules.txt").
 7> UpdatedMarketData = [{'GOOG', 505}, {'MSFT', 30}, {'AAPL', 158}].
 8> dsl:apply_biz_rules(UpdatedFunctions, UpdatedMarketData).
 Order placed: selling 400 shares of 'MSFT'
 Order placed: buying 7000 shares of 'AAPL'

结论

再重申一下我在文章开头所说的观点——Erlang是构建DSL的极佳平台。其优点远远不止匿名函数、正则表达式支持、模式匹配这几样。Erlang还允许我们编程访问经过词法分析(tokenized)、语法分析之后、抽象形式的表达式。Debasish Ghosh写的《string lambdas in Erlang》是另一个很好的例证。我希望本文能帮助一些读者走出自己熟悉的安乐窝,了解一点新的语法和编程范型。我还希望人们在给Erlang贴上专家语言的标签之前能够三思。

关于作者

Dennis Byrne就职于ThoughtWorks,一家全球性的咨询公司,专注于关键系统的全程敏捷软件开发。Dennis是开源社区的活跃分子,他于2008年6月为Erlang eXchange作了题为“Using Jinterface to bridge Erlang and Java”的演讲。

查看英文原文:Domain Specific Languages in Erlang


志愿参与InfoQ中文站内容建设,请邮件至editors@cn.infoq.com。也欢迎大家到InfoQ中文站用户讨论组参与我们的线上讨论。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

DSL是个框 by Zoom Quiet

逮到什么都能往里丢,,,

这DSL 说穿了,怎么看就象RESTful 变更一套种名词,,,

很有lisp的味道 by henyo ma

怎么这么像lisp?特别是那个抽象函数

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT