理解DSL

一个简单问题

如何使用程序来描述下面一个问题?

再一组候选人当中, 找到满足下列条件的数据:

  • 所有候选人性别为女
  • 数据按年龄排序
  • 是输出姓名以及年龄信息

ok, 问题已经抛出来了, 那么让我们用js来实现这个需求.

假设数据集叫data

1
2
3
data.filter((item) => item.sex === 'woman')
.sort((a, b) => a.age - b.age)
.map((item) => { name: item.item, age: item.ange });

上面的代码看上去, 再对js语言十分熟悉的人看来, 可以很快的看明白代码的意思, 但是如果是外行, 那可能有点难猜了, 那么有没有什么办法, 用更简单更易懂的方式来描述这个问题呢?

1
select 'name', 'age' from 'data' where sex = 'woman' sortBy 'age'

上面是一条SQL语句, 相信不管是不是程序员, 都能够快速理解这段语句的含义. 显然SQL再数据查询这个领域, 比js更加简洁, 表达力更强. 我们管这种再针对特殊领域的语言叫DSL(特殊领域语言)

什么是DSL

DSL是(Domain Specific Language)特定领域语言, 是做处理某一个特定领域的问题设计出来的语言, 这类语言比较简单, 再专属的领域内, 有非常强的表达力.

与之相对的是GPL(General Purpose Language) 通用编程语言. 这类语言是能够表达可被计算的逻辑, 必须是图灵完备, 他们的侧重点是灵活,全面. Java/python/js/go/c 这类都属于GPL

典型的DSL有哪些?

  • HTML
  • CSS
  • JSX
  • PUG
  • REGEX
  • SQL
  • XML
  • YMAL

上面的这些语言, 大家可以发现, 几乎都不是图灵完毕的计算机语言(为实现if空值/循环), 他们的语法很简单, 主要以声明式的表达方法来进行编写的

因为是使用声明式的表达方式, 所以在在阅读上会更加流畅

为什么要用DSL

DSL往往语法简单, 并且非常易于阅读, 再其擅长的领域内编码是非常高效的.
我们再拿HTML 和 js对象做一个对比:

1
{name: 'div', class: 'container', children: 'hello world'}

使用js来描述一个html节点, 只能把所有的属性平铺到一个对象中, 让人再阅读中无法找到重点, 并有效的区分区别

1
<div class="container"> hello world </div>

使用了xml语法, 可以快速辨别标签类型, 和内容, 整体语义化很强, 缺点就是有多余的符号和为了表达嵌套关系的闭合标签

1
div(class='container') hello world

使用pug独特的语法, 再不损失xml的表达力的前提下, 省略了无用的符号, 并通过缩进来表达嵌套逻辑, 降低了很多的语法噪音

可以看出来, DSL的引入确实能够有效的提高特定场景的表达效率, 再熟悉了相关语法后, 能够是代码更易理解, 也更加简洁

要不要引入DSL

再日常开发当中, 为了解决某一个场景的需求而引入一个新的DSL, 在我来看是需要谨慎考虑的. 因为这实际是引入了一种新的语言. 这可能会让整个项目的维护难度提升,
所以是否引入DSL需要考察几个点

  • 产能的提升是否能够抹平新语言引入带来的学习成本
  • 是否可以用相关第三方工具替代DSL

内部? 外部?

既然引入DSL是有成本的, 那么有没有什么成本比较低的引入方法?

也许有, 上面我们谈到的DSL在广义上作区分的话, 叫做外部DSL, 那么既然有外部DSL, 那肯定有内部DSL, 下面我举一个例子:

1
select 'name' from 'application' where id = 12345

1
2
3
select('name')
.from('application')
.where(id, '=', 12345)

上面的sql和下面的js代码实际上是等价的, 上面的sql我们叫它外部DSL, 下面的js叫做内部DSL.

再来一个例子:

外部DSL:

1
2
3
4
5
6
.container {
.content {
color: #fff;
margin: 10px
}
}

内部DSL:

1
2
3
$('.container .content')
.css('color', '#fff')
.css('margin', '10px')

显然这个内部DSL看着很眼熟, 感觉就是我们普通的js方法调用, 就是普通第三方库的接口调用. 实际上没有错, 内部DSL还有一个其他名字-流畅几口(fluency)

类似这样的内部DSL我们见过很多:

jquery:
JQuery可以被称作是对dom操作的内部DSL

1
2
3
4
$('.mydiv')
.addClass('flash')
.draggable()
.css('color', 'blue');

Moment
Moment 就可以被称作是对日期操作的 内部DSL

1
2
3
moment()
.subtract(10, 'days')
.calendar();

Chai
Chai 就可以被称作是对断言操作的 内部DSL

1
tea.should.have.property('flavors').with.lengthOf(3);

如何区分内部或外部DSL

通常来讲, 如果实现的功能,无法被宿主语言直接支持, 需要自己额外实现代码的编译和解析, 都属于外部DSL

如果实现的功能, 可以呗宿主语言直接支持, 就想上面的例子一样, 都是简单的函数形式, 那么这就属于内部的DSL

那么这里我有一个问题, 就是我们写React常用的JSX算是什么? 是内部的DSL么? 下面放一点示例代码

1
2
3
4
<Container>
<Menu list={this.state.list}/>
<Footer/>
</Container>

会被转成:

1
2
3
4
5
6
7
8
React.createElement(
Container,
null,
React.createElement(Menu, {
list: this.state.list
}),
React.createElement(Footer, null)
);

最然转义后的代码, 是合法的js代码, 但是源码并不是 一个合法的js代码, 正常的浏览器是无法正常识别这些代码的.
因为这些代码再被浏览器执行前, 会被Babel进行转移, 转移成可被浏览器识别的js代码而Babel再转义这些JSX的时候,
实际上内部已经实现了对这种特殊语法的编译和解析, 所以, 虽然jsx通常也写在.js文件中, 但它还是一个外部的DSL

如何看待内部DSL

DSL,实际上就是为了解决特定问题而出现的.
使用内部DSL来对某个领域进行扩展,在我看来是一个很好的解决方案, 因为没有引入新语言带来的新问题,

社区对,内部DSL的划分界定实际上很模糊, 希望大家不要过于纠结, 无论是API 还是 DSL, 能够有效的抽象并解决问题的, 都是好的解决之道

推荐阅读

前端 DSL 实践指南(上)—— 内部 DSL