如何理解DSL

从一个简单的问题开始:

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

在一组候选人中,找到满足以下条件的的数据:

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

问题跑出来,那么让我们使用JavaScript来实现这个需求:

假设数据集合叫data

1
2
3
4
5
const result = data.filter((people) => people.sex === '女')
.sort((cur, pre) => cur.age - pre.age)
.map((item) => {
return { name: item.name, age: item.age }
})

上面的代码看起来,在对于JavaScript语言十分熟悉的人来看, 可以很快的看明白代码的意思,但是如果是外行,那就可能有点难猜了。

那怎么办呢?有没有什么更好的办法/更简单易懂的方式来描述这个问题呢?

1
select 'name', 'age' from "data" where sex = "女" sortBy "age";

上面这个语句是一个SQL语句, 相信不管是不是程序员,都能快速的理解这段语句的含义。

显然SQL在查询这个领域比JavaScript更加简洁,表达力更强;我们管这种针对特殊领域的语言就叫DSL(特殊领域语言)。

什么是DSL

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

与之相对应的是的GPL(General Purpose Language) 通用编程语言。这类语言是能够表达科倍计算的逻辑,必须是图灵完备,他们的侧重点是灵活,全面。 Java,python,JavaScript,go,C 这类都属于GPL`。

典型的DSL 有哪些

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

上面的这些语言,几乎都不是图灵完备的计算机语言,他们的语法很简单,主要以声明式的表达方式来进行编写的。

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

关于什么是图灵机,什么是图灵完备等,可阅读这篇文章 什么是图灵完备?

为什么要用 DSL

DSL语法往往简单,并且非常易于阅读,在其擅长的领域内编码是非常高效的。

我们再拿 HTMLJavaScript对象来最一个对比:

1
{ name: "div", class: "container", children: "shuliqi" }

使用JavaScript来描述一个HTML节点,只能把所有的的属性平铺到一个对象中,让人在阅读中无法找到重点,无法有效的做区分。

1
<div class="container" > shuliqi </div>

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

1
div(class='container') hello world

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

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

要不要引入DSL

在日常的开发中, 为了解决某一场景的需求而引入一个新的DSL, 在我看来是需要慎重考虑的。因为这是引入了一种新的语言,这可能会让整个项目的维护难度提升。

所以否引入DSL需要考虑几个点:

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

内部DSL? 外部DSL?

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

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

1
select 'name' from 'applications' where id = 234
1
2
3
select('name')
.from('applications')
.where(id, '=', 234)

上面的SQL 和下面的JavaScript代码实际上是等价的, 上面的SQL代码我们叫它外部的DSL, 下面的JavaScript我们叫做内部DSL—-> knexjs

可以再来举个例子:

外部DSL:

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

内部的DSL

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

显然这个内部的DSL看着很眼熟,感觉就是我们普调的js方法调用,就是普调的第三方接口调用。

类似这样的内部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)
);

虽然转义后的代码是合法的javaScript代码,但是源代码并不是一个合法的javaScript代码,正常的浏览器是无法正常识别这些代码的。

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

如何看待内部DSL

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

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

文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2021/08/07/如何理解DSL/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog