scala下DSL的设计与开发

我们考虑scala下的DSL实现,scala有一些特性可方便的支持内部DSL开发:函数curry化、隐式转换、对象方法调用无需点号和圆括号。
假设有下面的DSL:
+ 支持int、string、bool类型的字面量
+ 支持in、not in操作,判断是否在一个tuple中,例如:”baba” in (“baba”, 1, 2)
+ 支持and、or操作符
+ 字面量支持len方法

内部DSL

先考虑内部DSL的实现。所谓内部DSL,就是不用专门的语法分析器,仅依赖宿主语言本身的语法特性来实现一门微型语言。
我们提供一个接受Any类型参数的Expr类来代表int、string、bool三种变量类型:

class Expr(v: Any) {
    def len = v match {
      case s:String => s.length()
      case _ => -1
    }

    def asBool = v match {
      case i:Int => i != 0
      case b:Boolean => b
      case s:String => s != null && s.length() != 0
      case _ => false
    }

    def in(args: Any*) = args.exists(arg => arg == v)

    def notin(args: Any*) = !args.exists(arg => arg == v)

    def and(that: Expr) = this.asBool && that.asBool

    def or(that: Expr) = this.asBool || that.asBool
}

注意:in和notin接受可变参数来模拟一个tuple。
为了支持”baba” in (“baba”, 1, 2)这样的写法,我们还需要隐式转换将int、string、bool等自动转成Expr:

object ExprConvertions{
  implicit def int2Expr(x:Int) = new Expr(x)

  implicit def str2Expr(x:String) = new Expr(x)

  implicit def bool2Expr(x: Boolean) = new Expr(x)
}

最后,这样来使用:

import ExprConvertions._
def main(args: Array[String])
{
    val e = "baba"
    println(e in ("baba", 1, 2))
    println(e notin ("babe", 1, 2)) 
    println(4 and 3)
    println(4 and 0)
    println(4 > 3 and 5 > 4)
    println(4 < 3 or 5 > 4)
    println("cia".len)
}

注意:由于隐式转换未放到Expr的伴生对象里,我们必须显式import ExprConvertions._
当然,如此做出来的DSL还有一些问题:
and、or不是短路的,且优先级无法保证;
notin不能写成not in。
and、or的问题很难解决,曾考虑试过传名参数(by-name parameter),这样写:

def and(cond: => Boolean) = this.asBool && cond

def or(cond: => Boolean) = this.asBool || cond

可以解决
println(4 > 3 and 5 > 4)
println(4 < 3 or 5 > 4)
两种写法的短路问题。
但会导致
println(4 and 3)
println(4 and 0)
根本编译不过,因为一个Int型并不是无参函数()=>Boolean(传名参数本质上就是无参函数,两者的字节码没区别)。
至于and、or的优先级问题就更难解决了,scala里+-*/并非真正的操作符,其优先级也是靠硬编码实现的^_^。

notin连写的问题有两种解决思路供参考
1、链式调用
可利用scala里的链式调用,比如:
instance func1 para1 func2 para2
等价于instance.func1(para1).func2(para2)
于是我们设想将not也作为Expr的一个成员函数,和in一起构成一个链式调用。但这种链式调用的省略原则仅针对带一个参数的函数有效,也就是说直接写not in,编译器会把in认为是not的参数,而非另一个成员函数。规避的方式也不是没有,我们可以使用“词对象”来做一个无实际语法意义的占位符,像这样:

object exist

class Expr(v: Any) {
  ......

  def in(args: Any*) = args.exists(arg => arg == v)

  def notin(args: Any*) = !args.exists(arg => arg == v)

  def not(x: exist.type) = new {
    def in(args: Any*) = Expr.this.notin(args)
  }

object exist就是所谓的“词对象”,把它作为not的参数完全是用来占位的。
注意,这里not会new出一个匿名类对象,其in方法是一个“否定的”in函数:
Expr.this.notin(args)
最终的写法像这样:
println(e not exist in (“babe”, 1, 2))
还算差强人意。

2、使用case类
case类与普通类的区别在于,scala编译器会自动为case类生成一些方法(例如toString、equals、hashCode等),还会为其生成一个object,因此支持“类名+括号”的方式new一个实例,不要小看这个特性,要知道,能少写一个new对于DSL的设计可是至关重要!
为支持not in的写法,我们考虑将in作为一个case类,同时将其作为not函数的参数,具体做法如下:

case class in(args: Any*)

class Expr(v: Any) {
  ......

  def notin(args: Any*) = !args.exists(arg => arg == v)

  def not(o: in) = this.notin(o.args)

这样调用:
Assert.assertTrue(e not in (“babe”, 1, 2))
类名in跟括号间可留空格。
至此,使用case类我们较好的解决了not in连写的问题。

外部DSL

前面说过,微型语言的很多特性受限于宿主语言自身的能力无法实现,比如and-or优先级、if-else写法,所以若要完整的实现这份微型语言,必须考虑外部DSL的做法。
在scala里我们可以使用parser combinator来做外部DSL,凑巧的是,该库本身恰好是一种内部DSL。
我们首先用parser combinator写出微型语言的语法:

class EquationParser extends JavaTokenParsers{
  def input =   ifExpr  | logicExpr

  def ifExpr = (atomExpr<~"if")~logicExpr~("else"~>atomExpr) 

  def logicExpr:Parser[Any] = andExpr~rep("or"~>andExpr) 

  def andExpr = notExpr~rep("and"~>notExpr) 

  def notExpr = opt("not")~relationExpr 

  def relationExpr = existExpr |
    relationExpr_i~opt("=="~relationExpr_i | "<>"~relationExpr_i) 

  def relationExpr_i = atomExpr~opt(">"~atomExpr |
    ">="~atomExpr |
    "<"~atomExpr |
    "<="~atomExpr) |
    "("~>logicExpr<~")"

  def existExpr = (atomExpr~opt("not")<~"in")~atomExprList 

  def atomExprList = "("~>repsep(atomExpr, ",")<~")"

  def atomExpr = genericStrLiteral  | arithExpr

  def genericStrLiteral = stringLiteral | """'[^']*'""".r

  def arithExpr: Parser[Any] = term ~ rep("+" ~ term | "-" ~ term) 

  def term: Parser[Any] = factor ~ rep("*" ~ factor | "/" ~ factor) 

  def factor: Parser[Any] = ident |
    floatingPointNumber  |
    "(" ~> arithExpr <~ ")"
}

看的出,上述写法跟EBNF表达式是很类似的,这依赖于scala的一些能力:
函数只有一个参数时可省略圆括号,比如f(a)可写成f a;
成员函数调用时可不用写点号,比如s.indexOf(‘a’)可写成 s indexOf ‘a’。
举例来说,def ifExpr = (atomExpr<~”if”)~logicExpr~(“else”~>atomExpr) 相当于
def ifExpr=atomExpr.<~(literal(“if”)).~(logicExpr).~(literal(“else”).~>(atomExpr))
哈哈,看到了吧,
<~
~
~>
全是atomExpr所返回的Parser类的成员方法。
这些省略原则使得原本只为程序猿所熟悉的函数链式调用看起来更像一种自然语言。

另外,我们也注意到parser combinator通过rep方法来支持kleene闭包(就是正则表达式里的*号),所以它应是一种LL(*)文法,因为如果是bison或PLY那样的传统的LR文法,用kleene闭包的地方得改成左递归。

最后一点,parser combinator是一种内部DSL,这是它的优点,使得它不用像bison或PLY那样专门设计一种外部DSL来表述EBNF;但内部DSL也是它的缺点,因为外部DSL写成的EBNF在转成宿主语言时会有一个静态的“文法翻译过程”, 该过程会在第一时间为我们找出文法中的所有错误,而内部DSL就没有这个能力了,问题只能在运行期才被发现,且报错信息几乎无指导意义。所以,对于较复杂的语言,使用parser combinator来设计恐怕会让人抓狂。当然,对于我们这里的微型语言,影响不大。

猜你喜欢

转载自blog.csdn.net/tlxamulet/article/details/77387579
DSL