Scala:一种统治DSL的语言(I)


/**
 * 谨献给我最爱的Yoyo
 *
 * 原文出处:https://scalerablog.wordpress.com/2016/04/18/scala-one-language-to-rule-them-all-i/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-02-07
 */

一开始,没有编程语言,只有机器代码。通过铸造能量环(即:编程语言)来统治机器,自动化编程拯救了我们所有人。他们多元化,启发了新的语言,构成了一个富生态系统。在它们中间涌现了一种高度有用的计算机语言:领域特定语言。

领域特定语言(DSL)

领域特定(计算机)语言已经存在有一段很长的时间了。对于这种语言,学术界的描述宣称它们是中心化的并且用于某一特定应用领域。领域特定语言简单而又简练,这意味着在描述动作、实体和应用领域内的关系过程中,它们会引导用户。更为重要的是,为了适配目标而制作它们。

如果我是从事统计研究的,为什么需要担心内存管理?

一点也不惊讶的是,这些迷你语言的整个王国已经进化并且接手了机器学习工具、科学编程、硬件设计与管理、建模等。。。

这不是关于scala的博客吗?把这些历史留给学者吧!

是的,这可能听起来像是过去的故事。然而,在当前的上下文,我们似乎全部都痴迷于数据获取、存储与分析,并且因为它的数量而把数据视为难以管理考虑在内。我们被迫使用数十种不同数据管理的方式处理数据,并且很多数据经由使用这些DSL:SQL、HiveQL、 CQL、bash、grep、awk、R、C’mon!根本列不完那么多!暂且忽略后面还有哪些。

假如有一个工具可以给予我们创造简单、具备引导性且简短的语言以便能在其中执行特定领域任务的能力,会是怎样?假如Scala是一种能够创造新的DSL的DSL(这里的D代表领域Domain),又会是怎样?!

当它的创始人命名为Scala时,他们不仅考虑到了代码重用和在横向并发环境的潜在使用的能力,还记住了这门语言的可扩展性。在那样方向下,一些相关的特性是:

  • 中缀表达式objectA.method(objectB)可以写成objectA method objectB
  • 缺乏操作符:没有类似分隔实例的操作符,只有方法。优先顺序和结合律由每个方法名字的最后字符提供。这样的话,任何不是用`:`结束的方法的名字从左到右进行结合:
    obj1 + obj2可以写成obj1.+(obj2),而obj1 +: obj2可以写成obj2.+:(obj1)
    类似地,操作符的顺序由方法名结束符的优先级列表提供。例如,`*`比`+`的优先级高,所以obj1 + obj2 * obj3总会解释成obj1 + (obj2 * obj3)。上述的优先级列表如下:

    • 任意符号,不区分大小写。
    • |
    • ^
    • &
    • 符号字面量 = 和 !
    • 符号字面量 < 和 >
    • :
    • 算术运算符 + 和 -
    • 算术运算符 *,/ 和 %
    • 任意其他指定字符

    面向对象的高级特性objecttrait。。。

这些特性可以用于合成模型并且构建Scala编程语言内部DSL

Scala DSL 101

中缀表达式是创建我们嵌入式语言主要的特性。

考虑以下特质:

trait MovingRobot {  
  def moveForward(): MovingRobot = {
    println("Robot moved one position forward")
    this
  }
  def moveBackward(): MovingRobot = {
    println("Robot moved one position backward")
    this
  }
}

可以这样用于混入某个对象的声明:

object robot extends MovingRobot  

它的方法可以使用传统的点表达式来调用:

robot.moveForward.moveBackward  

但中缀表达式给予了我们一种更自然的方式来和这个简单的机器人进行交流:

robot moveForward() moveBackward  
robot moveForward  
robot moveBackward  

这是所有可能的DSL当中最简单的了。

状态转换

是的,简单但相当粗糙且没用。这些命令没有改变系统状态,除了在println背后的副作用:
现在,对DSL指令的副作用进行建模有两种选择:

  • 可变的方式:某种程度上来说,对于来自命令式语言的Scala的新手这是最简单的方式,但也更多bug的方式。这种方式与很多Java的构造器所遵循的方式非常相似。请参考Java里的StringBuilder

    这个构造器的状态是组合而成的字符串。例如append(double d)方法,返回一个状态已经被相同方法修改的构造器实例的引用。所以,总会返回相同的引用,返回一次又一次调用后修改过的相同的StringBuilder实例,听起来是不是很熟悉?!

  • 不可变的方式(或者说是明智之选):不要改变任何东西,返回一个带有派生自前面一个状态的属性的新状态。从现在起,这篇文章将只会涵盖这种方式。

第二种解决方案之美在于每个动作返回一个新的状态对象,与系统状态的关系是1对1的。那就是说,代码实体是所有变化完美的映射。此外根据定义状态也是不可变的。

state /steɪt/ n., adj., v., stat•ed, stat•ing.
n.

  1. the condition of a person or thing with respect to circumstances or experiences; the way something is[countable; usually singular]the state of one’s health.
  2. the condition of substances with respect to structure, form, etc.[countable]Water in a gaseous state is steam.

(www.wordreference.com)

讨论为什么不可变性驱动更少缺陷的系统超出了本文的范围,通过谷歌“不可变性减少bug”可以找到上页种解析。甚至Java创始人也认可这一点,至少对于他们的字符串来说。
每次转换返回一个全新的状态从而把它的职责减少到只有一个:生成一个新的状态从而简化DSL设计。在显式转换调用里,期望得到的是不发生任何改变的状态。

不可变状态转换的核心

继续我们单向机器人接口这一完全复杂的例子(现在你肯定已经意识到先前Scala的挑战包含了一个美丽的SDL),它可以这样修改以便可以遵循上述函数式的方式:

// All states extend `RobotState`
trait RobotState {  
  def position: Int
}

// Transitions which can be mixed with any state for which they
// make sense.

trait MovementTransitions {  
  self: RobotState =&amp;gt;

  def moveForward(nSteps: Int = 1): RobotState with MovementTransitions

  def moveBackward(nSteps: Int = 1): RobotState with MovementTransitions

}

// States
// In this example, states only differ in the robot position so they all
// are represented by the same case class.
case class Robot(position: Int) extends RobotState with MovementTransitions {

  def moveForward(nSteps: Int = 1) =
    Robot(position + nSteps)

  def moveBackward(nSteps: Int = 1) =
    Robot(position - nSteps)

}

// Initial state
val robot = new Robot(0)  

它的使用是:

robot moveForward(10) moveBackward() position  

上面的代码过度单纯化,但展示了在Scala领域特写语言背后基本的技巧,即:对于中缀表达式、状态家族和只在状态定义内可用的转换的使用。

一丢丢理论:不是吧?状态机?

实现DSL真的需要状态机模型吗? 是的,如果你不想自己打自己的脚的话。

不可变的状态机易于理解、维护和扩展。

而别一方面,领域特写语言是语言,是带有语法的正式语言,并在Noam Chomsky分类里有一席之地,通常地,有常规语法( Regular Grammar)和上下文无关(Context-Free )语法。

  • 哪种理论机器可以用于识别/处理常规语法的语言?有限状态自动机
  • 在上下文无关语法语言的情况下,它们可以被下推自动机来处理,(注意!超前过度单纯化)这可以被视为有限自动机,享受利用其自己的栈来放置和读取符号的特征。

前面描述的转换模型看起来只是用于实现这种机器。抛出一个自问自答的问题,当有一个如此坚固的模型可用时,领域特定语言开发人员是否还要投入精力去寻找幼稚而脆弱的解决方案?

  • 好吧,我建议你们这些先生发明一种能把方钉放进一个圆孔的方式。
  • 先生,我们有大量圆钉!

接下来。。。

在下一篇文章:手把手教你开发实用的DSL,不会再有什么历史啊,理论啊或者没人关心的杂七杂八的东西!只有。。。

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州