语法噪音

2008年6月9日

在谈论领域特定语言(或任何计算机语言)时,一个经常被提到的词是语法噪音。人们可能会说Ruby比Java噪音小,或者外部DSL比内部DSL噪音小。语法噪音指的是那些并非我们真正需要表达的意思,而是为了满足语言定义而存在的额外字符。噪音字符是不好的,因为它们会掩盖程序的含义,迫使我们费力地去理解它的作用。

像许多概念一样,语法噪音既松散又主观,这使得它很难讨论。不久前,Gilhad Braha在JAOO的一次演讲中试图说明他对语法噪音的看法。在这里,我将尝试一种类似的方法,并将其应用于我在当前DSL书籍介绍中使用的几种DSL公式。(为了保持文本的合理大小,我使用了状态机示例的一个子集。)

在他的演讲中,他通过对认为是噪音字符的部分进行着色来说明噪音。当然,这样做的问题在于,这需要我们定义什么是噪音字符。我将避开这一点,做一个不同的区分。我将区分我所说的领域文本和标点符号。我正在查看的DSL脚本定义了一个状态机,因此会谈论状态、事件和命令。任何描述我的特定状态机信息的内容——例如状态的名称——我都将其定义为领域文本。其他任何东西都是标点符号,我将用红色突出显示后者。

我将从外部DSL的自定义语法开始。

events
  doorClosed  D1CL
  drawOpened  D2OP
  lightOn     L1ON
end
   
commands
  unlockDoor D1UL
  lockPanel   PNLK
end
   
state idle
  actions {unlockDoor lockPanel}
  doorClosed => active
end
   
state active
  drawOpened => waitingForLight
  lightOn    => waitingForDraw
end

自定义语法倾向于最小化噪音,因此您会在这里看到相对少量的标点符号。这段文字也清楚地表明,我们需要一些标点符号。事件和命令都是通过给出它们的名称和代码来定义的——您需要标点符号来区分它们。所以标点符号不等于噪音,我认为错误的标点符号或过多的标点符号才是噪音。特别是,我认为试图将标点符号减少到绝对最小值不是一个好主意,过少的标点符号也会使DSL更难理解。

现在让我们看看Ruby中相同领域信息的内部DSL。

event :doorClosed, "D1CL"  
event :drawOpened,  "D2OP"  
event :lightOn, "L1ON"  

command  :lockPanel,   "PNLK" 
command  :unlockDoor,  "D1UL" 

state :idle do 
  actions :unlockDoor, :lockPanel
  transitions :doorClosed => :active
end 

state :active do 
  transitions :drawOpened => :waitingForLight, 
              :lightOn => :waitingForDraw
end 

现在我们看到了更多的标点符号。当然,我可以在我的DSL中做出一些选择来减少标点符号,但我认为大多数人仍然同意Ruby DSL比自定义DSL有更多的标点符号。这里的噪音,至少对我来说,是一些小东西:标记符号的“:”,分隔参数的“,”,引用字符串的“”。

我的DSL思想中的一个主要主题是,DSL是一种填充框架的方式。在这种情况下,框架是描述状态机的框架。除了使用DSL填充框架之外,您还可以使用常规的按钮式API来填充框架。让我们为其中的标点符号着色。

Event doorClosed = new Event("doorClosed", "D1CL"); 
Event drawOpened = new Event("drawOpened", "D2OP"); 
Event lightOn = new Event("lightOn", "L1ON"); 
 
Command lockPanelCmd = new Command("lockPanel", "PNLK"); 
Command unlockDoorCmd = new Command("unlockDoor", "D1UL"); 

State idle = new State("idle"); 
State activeState = new State("active"); 
 
StateMachine machine = new StateMachine(idle); 

idle.addTransition(doorClosed, activeState);
idle.addCommand(unlockDoorCmd);
idle.addCommand(lockPanelCmd);

activeState.addTransition(drawOpened, waitingForLightState);
activeState.addTransition(lightOn, waitingForDrawState);

这里有很多标点符号。各种引号和括号,以及方法关键字和局部变量声明。后者提出了一个有趣的分类问题。我将局部变量的声明算作标点符号(因为它重复了名称),但将其后来的使用算作领域文本。

Java也可以用流畅的方式编写,所以这里是书中的流畅版本。

public class BasicStateMachine extends StateMachineBuilder { 
  Events doorClosed, drawOpened, lightOn; 
  Commands lockPanel, unlockDoor; 
  States idle, active; 

  protected void defineStateMachine() { 
    doorClosed. code("D1CL"); 
    drawOpened. code("D2OP"); 
    lightOn.    code("L1ON"); 

    lockPanel.  code("PNLK"); 
    unlockDoor. code("D1UL"); 
 
    idle 
        .actions(unlockDoor, lockPanel) 
        .transition(doorClosed).to(active) 
        ; 
 
    active 
        .transition(drawOpened).to(waitingForLight) 
        .transition(lightOn).   to(waitingForDraw) 
        ; 
 } 
 

每当两三个人聚在一起谈论语法噪音时,XML必然会被提及。

<stateMachine start = "idle"> 
    <event name="doorClosed" code="D1CL"/>  
    <event name="drawOpened" code="D2OP"/> 
    <event name="lightOn" code="L1ON"/> 

    <command name="lockPanel" code="PNLK"/> 
    <command name="unlockDoor" code="D1UL"/> 

  <state name="idle"> 
    <transition event="doorClosed" target="active"/> 
    <action command="unlockDoor"/> 
    <action command="lockPanel"/> 
  </state> 

  <state name="active"> 
    <transition event="drawOpened" target="waitingForLight"/> 
    <transition event="lightOn" target="waitingForDraw"/> 
  </state>
</stateMachine> 

我认为我们不能过多地解读这个特定的例子,但它确实提供了一些思考。虽然我认为我们不能严格区分有用的标点符号和噪音,但领域文本和标点符号之间的区别可以帮助我们关注标点符号,并考虑哪些标点符号最适合我们。我还要补充一点,如果DSL中的标点符号字符比领域文本字符多,那就是一种代码异味。

(Mikael Jansson发布了一个Lisp版本的例子。Mihailo Lalevic用JavaScript做了一个。)