erlang:用Erlang实现领域特定语言

  人们对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'
实现  前 3行echo命令创建biz_rules.txt文件并向其中写入 3条规则这些规则逻辑很直白;其表达形式和直接从用户口中说出来话相差无几

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”这些关键字这 3个关键字的于栈就像是“malloc”和“dealloc”的于堆……多余

  给仿真器第 2行命令是产生个匿名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 = :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:(Rule),
   [{_,_,Action},{_,_,Quantity},_,_|Tail] = Scanned,
   [{_,_,Ticker},_,_,_,{_,_,Operator},_,{_,_,Limit}] = Tail,
   to_function(Pid, Action, Quantity, Ticker, Operator, Limit).
  第行代码将传给rule_to_functionRule串扫描进个元组(tuple)接下来两行用模式匹配摘出实施规则所必需数据摘出来值被绑定到Quantity、Ticker(译注:股票代码)等变量接着broker进程ID和摘出5个值被传给to_function to_function将构建并返回代表着条业务规则

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

to_function(Pid, Action, Quantity, Ticker, Operator, Limit) ->
   fun(Ticker_, Price) ->
    
     Ticker =:= Ticker_ andalso
     ( ( Price < Limit andalso Operator =:= less ) or
      ( Price > Limit andalso Operator =:= greater ) ) ->
       Pid ! {Action, Quantity, Ticker}; % place an order
     true ->
       erlang:display("no rule applied")
    end
   end.
  这个to_function实现做了件事——返回个匿名匿名参数是两项市场数据:股票代码和价格传给该股票代码和价格将和业务规则中指定股票代码及价格相比较如果匹配上了就用发送操作符(即!符号)向broker进程发送则消息告诉它下单

  加载业务规则第 2种途径  to_function第 2种实现学院味比较浓它构造种抽象形式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:_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 = Operator =:= greater -> '>'; true -> '<' end,
   [{'',1,
    [{clause,1,,
     [[{op,1,
       'andalso',
        {op,1,'=:=',{atom,1,Ticker},{var,1,'Ticker'}},
        {op,1,Comparison,{var,1,'Price'},{eger,1,Limit}}}]],
     [{op,1,
       '!',
       {var,1,'Pid'},
       {tuple,1,[{atom,1,Action},
            {eger,1,Quantity},
            {atom,1,Ticker}]}}]},
    {clause,1,,
     [[{atom,1,true}]],
     [{call,1,
       {remote,1,{atom,1,erlang},{atom,1,display}},
       [{,1,"no rule applied"}]}]}]}].
  rule_to_abstract构造并返回个抽象形式Erlang控制结构这个控制结构是由系列具体限制条件组成个“”语句顺便上面抽象形式用了后缀运算符写法区别于般Erlang语法中缀写法如果把它套用到单条规则上面我们实际上是用编程方式构建以下代码抽象形式:

     
        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这 3个变量被从执行作用域中抓出来经由内建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).
  幸亏我们只有 3条业务规则apply_biz_rules运行时是指数增长不熟悉Erlang语法或者不留心听讲上面算法读起来有点难懂apply_biz_rules个内部映射到市场数据中对股票代码/股价内部又将第 2个内部映射到每个业务规则第 2个内部再将股票代码和股价传递给业务规则

  执行apply_biz_rulesbroker进程确认它收到买入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, "_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写 lambdas in Erlang是另个很好例证我希望本文能帮助些读者走出自己熟悉安乐窝了解点新语法和编程范型我还希望人们在给Erlang贴上专家语言标签的前能够 3思



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



Tags:  erlang公式 erlang分布 erlang程序设计 erlang

延伸阅读

最新评论

发表评论