热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

浅谈领域特定语言

目录0序言1什么是DSL1.1生活中的DSL1.2DSL与GPL1.3DSL的分类1.4DSL的优势与劣势2建立一个简单的DSL2.1领域建模与问题域和解答域2.

目录

0  序言

1  什么是 DSL

1.1 生活中的 DSL

1.2 DSL 与 GPL

1.3  DSL 的分类

1.4  DSL 的优势与劣势

2  建立一个简单的 DSL

2.1 领域建模与问题域和解答域

2.2 确立共通的词汇

2.3 设计 DSL 执行模型

2.4 初次尝试 DSL

3  DSL 的优势与发展

3.1 eDSL 优势与分类

3.2 业界语言 eDSL 扩展能力分析

3.3  UI 领域的 eDSL —— SwiftUI

4  总结

4.1 小结

4.2 未来的展望




0  序言

 

近年来,随着移动应用、人工智能、区块链、量子计算、芯片逻辑、音乐工程等领域的蓬勃发展,软件在各行各业的应用得到爆发式的增长,各行各业的研究人员和领域专家加入到软件开发的队伍,比如:芯片研发工程师需要通过编程来写芯片逻辑,人工智能算法研究人员需要通过编程写计算逻辑,音乐工程师需要通过编程构建音乐的交互式系统。这些领域专家不是计算机科班出身,但是他们对自己的领域研究非常深入,他们非常期待能有一门面向领域的编程语言来帮助他们屏蔽计算机底层的复杂度,聚焦自己的领域,高效的进行编程。

在这样的背景下,领域特定语言如雨后春笋般在各行各业兴起。比如苹果在 2019 年推出基于 Swift 的 UI 描述语言 SwiftUI,FaceBook 在 2019 年推出安全灵活的 Libra 区块链编程语言 Move,微软在 2017 年推出量子计算编程语言 Q#,伯克利使用高层次构建语言 Chisel(基于 Scala 语言扩展)编写 RISC-V 的芯片逻辑,因此业界对领域特定语言的研究也进入了一个高峰期。其实领域特定语言(DSL)的概念并不是新的概念,它最早可以追溯到上世纪 50 年代,比如科学计算领域使用的程式语言 Fortran。而领域特定语言真正得到快速发展,还是得力于领域特定语言在理论、工程技术、工具等方面的发展。

本文主要介绍领域特定语言(DSL)的基本概念、分类以及优劣势,并通过一个简单的例子入手讲解如何构建一个领域特定语言,并且结合 EDSL 的优势以及发展趋势,推导出宿主语言的特性需求,最后通过一个工业界的典型 DSL——SwiftUI 讲解元语言和对象语言的配合关系。

当然如果读者想了解更多有关 DSL 相关的技术内容,可以持续关注 SIG-DSL 扩展发表的相关文章。我们也欢迎读者朋友加入我们的 编程语言技术社区 SIG-DSL 扩展小组 ,和我们一起深入探讨 DSL 相关技术。

加入方式:文末有小助手微信,添加并备注加入 SIG-DSL 扩展




1  什么是 DSL


1.1 生活中的 DSL

单看上面的叙述,对于初次接触这一领域的你来说,大概率还是会迷惑什么是 DSL,引用维基的定义:领域特定语言指的就是专注于某个应用程序领域的计算机语言 [1]。现在我们将这一句话换一种说法:领域特定语言,就是让解决领域特定问题的编程模型使用领域专有的语言来 “说话”。即用代码来说话,其目的是让用户通过阅读代码就可以明白建模里的业务规则,而非一堆架构图上的框框和箭头。

如果要弄懂这个概念,首先我们要明白什么是领域专有的语言,这里举用一个和 Debasish Ghosh 所提到的买咖啡相似的用例 [2],这就如同,当你走进奶茶店点上一杯 “正常冰 3 分糖的多肉葡萄”,那么店员就会准确无误的给你制作一杯,550ml 当季葡萄果肉加绿茶底并配上醇香芝士再加上满满一杯子冰的饮品。你无需过多的描述每一项,只需按照菜单规则的名字来点单,而且这一说法只适用于奶茶饮品领域。

在这个场景中,点单的过程就是用特定领域的语言来表达一个问题,店员根据你的订单,把饮品做出来是对于问题的解答。这种从问题域映射到解答域的实现模型就是 DSL 的基本思路。如果把上述的场景做成软件,那么客人们点单时所用到的语言就是你要找的 DSL。


1.2 DSL 与 GPL

认识一个新事物,我常常会从正面和对立面两个方向来看,正面就如上段内容所述,而与 DSL 处在对立面的就是 GPL(General Purpose Language),即通用编程语言,像我们熟悉的 Java、Python 或者 C 语言等,这类语言没有特定的使用场景,你可以拿他们来实现任意领域的计算机程序,他们的出现并不是为了解决特定领域的问题,我们称这种能力为具有通用的表达力。

与其相反,DSL 都有相对应的专门的领域,在生活中常见的 DSL 如:HTML 是我们熟知的用于描述 Web 页面的标记语言;CSS 是用于描述页面样式的语言;SQL 是用于创建和检索关系型数据库的语言;这些语言都有一个特点,就是只能解决其特定领域的问题,正如你不可能拿一把菜刀来修汽车一样,你同样无法单独用 CSS 来搭建出一套证券交易系统,这也是正是 Martin Fowler 提出的 DSL 与 GPL 的根本区别,即 Limited Expressivity (有限的表达力)[3]。

通用编程语言可以给任何事物建模,而 DSL 只适合给一个专门的领域建模。对于 DSL 与 GPL 的区别,可以借用编程语言 Lab 架构师徐潇在 QCon 大会上提出的一个例子来阐明,他将两种语言比作建筑材料,GPL 如水泥,而 DSL 如水泥做好的 “预制件”。


图片
图 1:GPL 与 DSL 区别的形象解释

水泥可以做成任何建筑物,但是水泥管只能限定于做管道,而无法应用成楼板或其他建筑组件。但是当我们想要做管道的时候,我们更愿意接受可以直接使用的 “预制件”,而不是每次都从水泥铸型开始。这一例子也正是生动形象的描述出了 DSL 与 GPL 在表达力和易用度上的根本差异。


1.3  DSL 的分类

DSL 本身是一个宽泛的概念,它还可以细化成外部 DSL 和内部 DSL(Embedded DSL,简称 eDSL),像上述所列出的 HTML、CSS 以及 SQL,都是完全独立的语言,他们不依托于任何一门通用的宿主语言,我们称这种 DSL 为外部 DSL。

与之相对应的,如在 UI 领域,基于 Swift 建立的 SwiftUI;基于 Ruby 设计的 IOS 依赖管理组件 CocoaPods;基于 Kotlin 或者 Groovy 设计的 Android 主流编译工具 Gradle;这些在现有宿主语言上实现的 DSL,我们称之为 eDSL。

关于 eDSL 的详细介绍与优势对比,我们将在后续章节详细展开。


1.4  DSL 的优势与劣势

既然 DSL 的应用如此广泛,那么是否所有的项目都应该基于 DSL 开发?答案显然是否定的,没有一门技术具备完美解决所有问题的能力,DSL 同样在显著的优势下暗藏着危险,在确认是否引入 DSL 之前,我们需要评估一下 DSL 的优缺点。

首先从 DSL 所扮演的角色来看,由于 DSL 更趋向于解决固定范围的事情,所以基于 DSL 的开发关注点会更加的集中,它仅需要提供范围足够小的一个领域的 API 接口,并且会将领域中的专有概念抽象成更加精确的统一语言来表达其语义(这一点将会在下一章节中说明)[2],这也就意味着 DSL 在有限的范围内具有更强的表现力,且可以消减开发人员与领域专家之间沟通的代沟。

同时 DSL 是一层更高层的抽象,并不需要使用者懂得基于低层次语言的实现手法,所以对于没有编程经验和背景的领域用户来说,稍加学习便可以使用其来实现自己想要的程序,这也正是 DSL 最大的魅力。

当然缺点也同样存在,首先不可规避的是 DSL 是一种语言设计类的工作,而要从零设计一门语言,需要设计相关的语法、语义,同时还需实现其对应的编译器(语法解析、类型系统和代码生成等),这并不是一个靠堆积人力就能解决的问题,而是对复杂度和专业度要求很高的工作,所以越来越多的人会将 DSL “寄身”于别的宿主语言,也就是所谓的 eDSL。即便如此,eDSL 的设计难度还是很高,对于 eDSL 的介绍将在第三章展开,这也将是本文的重点内容。

再者就是一些配套开发工具的开发,如果从零开始设计一门语言,那么像 IDE、测试工具、调试工具和性能工具等相关配套工具都需要从零开始设计并实现,而 eDSL 可以让开发者复用宿主语言本身就具备的这些工具,这也是越来越多的人选择 eDSL 的原因之一。当然对于一些场景, IDE 或者测试工具还是有可能会需要基于宿主语言原生的工具做一些相对的扩展。

另一方面,如果越来越多的外部 DSL 出现,那么对于开发者来说,将会出现语言永远学不完的现象,任何一个外部 DSL 出现,都需要开发者从零开始学习一门新的语言,而且由于这门新语言只针对特定领域,导致其使用场景有限,这也会带来学习成本与学习价值之间失衡的现象,这是另一个人们选取 eDSL 的原因,毕竟 eDSL 只需要开发者学习基于宿主语言新抽象出的接口即可。


图片
图 2:学不完的外部 DSL [4]




2  建立一个简单的 DSL


2.1 领域建模与问题域和解答域

想要设计一款 DSL 需要从领域建模入手,领域建模用于以可视化的形式记录一个系统中的所有的关键概念和词汇表,并且描述出系统中各个实体之间的关系。在被用于软件开发阶段中,实体则可以演变为类别,方法和交互关系则可以演化为逻辑代码。上文中所提及的 “问题域” 和“解答域”则是领域建模的基本实践活动。

那么什么是问题域?问题域是指我们在业务场景中的实体,过程和约束条件,如前面的例子中,芝士、冰、甜度、多肉和葡萄等这些主要元素即为问题域中所有的实体,其中葡萄和杨梅无法同时放入等规则是一些加在实体上的一些约束条件,再复杂的模型还会添加一些其他的行为,如是否需要打包、何时取餐等。

解答域则为实现问题域的分析模型提供手段,换言之,问题域所有的元素都需要映射成解答域中的一种手段,如问题域中的冰度,则会映射到解答域中加冰这一解决方法上。而且这种组合必须符合映射的概念,即每个问题域在解答域中必须有且仅有唯一的元素与其对应,不能出现单对多或单对零的情况出现。如果是单对多,那么说明我们设计的解答对于当前的问题域出现了歧义;如果是单对零,那么说明我们设计的解答对于当前的问题域是不完备的。下图描述了领域建模的第一步,左边所有的实体,都可以在右边找到对应的表示。


图片
图 3:问题域到解决域的映射


2.2 确立共通的词汇

现在我们已经有了问题域和解答域,并且我们已经知道问题域和解答域的映射关系,那么设想如下场景:背景是作者接手了一个大型金融中介机构的后台操作建模的项目,有一次在参加金融中介机构的一次需求分析大会时,发现金融业的领域专家在会上大谈一些附息债券、折价债券等金融中介的常用术语,这便对建模实施者,也就是作者本身造成了困扰,因为对于作者而言,他并不是金融领域相关的从业者,所以不理解这些专有的词汇,甚至容易混淆一些同义词,这就导致作者难以明确最终的需求 [2]。(该实例来自于 Debasish Ghosh 《领域专用语言实战》一书)

这里需要注意的是,这段故事中提到的两种人物身份,一类是身为领域相关从业者的领域专家,还有一类是身为建模人员的作者本身,当建模人员不熟知领域相关知识的情况时,那么领域专家与建模人员之间的交流便存在了问题。

所以不难想出,为了解决这一问题,我们需要构建一门统一的词汇。在 “领域驱动设计之父” Eric Evans 所写的《Domain-Driven Design — Tackling Complexity in the Heart of Software》一书中,Eric 提出了 Ubiquitous Language(统一语言)[5],这一词的目的是创造一门可以在团队、开发者、领域专家、测试人员以及其余参与者之间共享的语言,而这种共通的词汇也正好是联系领域专家与建模开发者之间的桥梁。

试想一下如果没有共通的词汇的存在,那么也就没有了一一对应的关系,这对于需求对齐来说是致命的,如此多的陌生词汇,甚至还参杂着同义词、近义词在其中,这些对于领域建模来说都是或多或少的噪音干扰。因此在领域建模的过程中我们需要设计这一门统一语言来作为需求对齐、会议研讨甚至测试编写的唯一语言,对于那种相同含义的词汇就应该按统一的一种名字来命名,这也正是 Eric 所强调的去除领域理解中难懂的术语与结构,避免设计中的歧义以及需求的不一致。

请一定要注意这关键的一步,共通词汇的确立是需要时间反复提炼打磨的,它可以避免更多因需求没对齐而带来的返工开销。这就如同武侠小说中的内功心法一样,只要内力足够浑厚,后续只要学会了一两招的招式,那便是响当当的武林高手。

所以统一语言的构建是建模的前提基础,有时我们在这上面花的时间甚至会比预期想象的要多得多。我们再回到上述买奶茶的例子,在确立共通的词汇前,我们问题域的一些实体,虽然小有体系,但却是杂乱无章的,甚至还会有混淆重叠的部分,如下图示:


图片
图 4:统一词汇前的问题域

当建模者时而听到三分甜,时而听到 30% 糖度,又有时会听到 0.4Oz,虽然三者意思相近,但是对于建模者来说难免需要进一步思考三者的联系与差别,这还仅仅是一个词汇相对通俗易懂的场景,如果我们拿上文中所提到的金融中介的场景来看,那么这个问题的复杂程度可想而知,可不是简单的 “糖度” 可以比拟的。

这时候统一语言的好处就显示出来了,当我们把所有的词汇都按照规定的统一语言规定好之后,对于上述场景,我们就统一规定这一规模的糖度为三分甜,那么对于建模者来说,这就是一档糖度的表示,同时对于领域专家来说,这就是代表着对于 500ml 的饮品需要添加 0.4Oz 的糖。这样当领域专家再和建模者讨论的时候,就达成了一致,而此时,我们上述的图就可以简化为下面的样子:


图片
图 5:统一词汇后的问题域

 同时好的统一语言对于 DSL 的设计是有着极大的正向促进效果的,建模者完全可以就把设计好的统一语言转换为 DSL 中的各个实体,比如在统一语言中,我们设置糖度为五个专有词汇 “全糖”、“七分糖”、“半糖”、“三分糖” 和“无糖”,且糖的种类可以分为 “果糖” 和“糖浆”,那么当建模者在设计一个添加糖度模块的时候,同样可以使用这些名字,如定义一个糖的对象,它具有糖度和种类两种属性,并设置这些属性的值就为这些统一语言规定的词。

所以当我们的统一语言设计的足够好的时候,那么对应的 DSL 的实体的定义与词汇也就呼之欲出了。再进一步需要做的就是设计一层足够好的抽象和映射逻辑。回头看看,现在我们已经花了足够多的时间在领域词汇的确立和模型分析上,当然这一部分的开销是值得的,那么我们如何才能让领域专家直接基于系统的输入得到解答域的技术制品?或者说是否可以以上述设计的统一语言为基础,建立一种沟通模型,让无论是领域专家还是其他更多参与者都可以抛出问题并得到解答域制品?而这正是我们所需要的 DSL。


2.3 设计 DSL 执行模型

上文说到我们需要靠 DSL 来实现中间的映射关系,那么我们之前的图片就可以转换为下图:


图片
图 6:DSL 作为映射桥梁

 一个好的 DSL 脚本在联系两个区域时需要遵守一定的原则,Debasish 认为在设计 DSL 应该体现以下三项原则 [2]:


  • DSL 需要将问题域中的实体以同名的形式创建相对应的抽象

  • DSL 脚本的逻辑语言与词汇必须使用设计好的统一语言

  • DSL 脚本需要对语言实现进行抽象,DSL 脚本中不应该引入与解决特定领域问题本身无关的复杂逻辑

显而易见的是这些原则在极力地用领域用户能看懂的方式来建立 DSL,并且尽力避免掉一些与领域用户无关的干扰,其根本目的就是为了加强与领域用户的沟通能力,使领域用户稍加学习便能基于 DSL 做出他们想要的应用程序。那么 DSL 脚本到底是如何执行的呢?


图片
图 7:三种常见的 DSL 运行方式

 上图展示了三种比较常见的 DSL 脚本执行方式:


  1. 源码可以直接执行

  2. 源码会编译成可执行的二进制

  3. 源码利用元编程等能力转译为另一份源码

前两种并无过多需要描述的,直接执行就是指我们所建立的 DSL 基于的语义模型是解释执行的语言,类似于 Python 或者 Javascript;而第二种就是指基于的语义模型是编译执行的语言,像 Java 或者 C++;第三种方式则被大量运用在 eDSL 之中,其利用宿主语言所具备的元编程的能力,将宿主语言的复杂性简化从而提高 DSL 的表现力和简洁度,对于元编程的介绍与优势将在后续的章节进行展开。


2.4 初次尝试 DSL

万事俱备,现在让我们回到最初的奶茶的那个例子,我们尝试着利用本章节所讲到的方法设计一个针对奶茶行业的简单 DSL,并用其做一个奶茶订单系统。

在此之前我们需要做一些假设与背景故事的丰富来增强我们的体验感。我们假设现在有一台可以自动做奶茶的机器,它无需店员亲自手动的来搭配各种原料,店员仅需要依靠该奶茶制作系统点单,系统会依靠点单生成对应源码并运行调用奶茶机提供的接口,后者将自动做出店员想要做的奶茶。为了简化场景,我们的奶茶店仅有 3 种底料可供选择:杨梅汁、葡萄汁以及牛乳,对应的茶底也仅有三种:乌龙茶、红茶以及绿茶,同时我们有五种甜度(全糖、7 分糖、半糖、3 分糖和无糖)和四种温度(正常冰、少冰、无冰和加热)供用户选择。

首先,我们先确立一下共通的词汇,由于本场景比较简单,上面的词汇已经能够很自然的作为我们所需要的共通词汇了。总结出如下表:


图片
表 1:统一词汇表

现在有了共通的词汇,那么我们按照上文所提到的三项原则可以确定如下实体,我们采用枚举类型来表示出一杯饮品的各种属性,以及属性的值,在此次模型中,我们假定有这样一门宿主语言,它具备类型系统并可以与 Javascript 互操作,我们将基于这一门宿主语言实现我们想要的 eDSL:

// use enum to represent the tea taste
ENUM Taste {bayberry | grape | milk
}

// tea types
ENUM Tea {Oolong | black | green
}

// the sweetness types
ENUM Sweetness {full | threeQuarter | half | oneQuarter | noSuger
}

// the temperature
ENUM Temperature {ice | halfIce | noIce | heat
}

有了各种属性之后,我还需要定义一个订单的类,用于生成最后的订单发送到制作奶茶的机器:

Class Order {constructor(taste, tea, sweetness, temperature) {let taste: Taste = taste,let tea: Tea = tea,let sweetness: Sweetness = sweetness,let temperature: Temperature = temperature}
}

现在我们已经有了订单,那么我们还需要一些行为来与奶茶店的设备做一些交互,比如假设奶茶制作机提供给我们一个 Javascript 的接口 makeTea,接口的参数类型是 Object,并且,我们奶茶店的大屏也会提供给我们一个 Javascript 的接口 showPrice,用于显示最终的价格(假定只按口味区分价格,分别为 15、14、16),那么我们现在借助语言互操作的能力给订单赋予这种能力:

/* 借用语言和 Javascript 互操作的能力,调取外部 machineAPIs 模块中的接口 */
JSModule("machineAPIS") {func makeTea(order: Order): voidfunc showPrice(price: String): void
}
Class Order {...func getPrice() {let price = Switch (this.taste) {case: bayberry => "15"case: grape => "14"case: milk => "16"}return price}func sell() {makeTea(this)showPrice(this.getPrice())}
}

以上我们就简单地创建了一个极简的模型,那么当用户下发订单的时候,店员就可以直接用下面的代码实现:

// 订单 A
let orderA = Order(Taste: bayberry, Tea: Oolang, sweetness: halfSweetness, temperature: ice)
orderA.sell()
// 订单 B
let orderB = Order(Taste: bayberry, Tea: black, sweetness: halfSweetness, temperature: heat)
orderB.sell()

我们由此可以发现 DSL 的神奇所在,即便你从未接触过这门假想中的编程语言,你仍然能够很轻易地理解这段代码在干嘛。下面我们再借用元编程的能力,使其变的更酷一些,我们可以通过宏定义的方式将上述代码中的一部分代码隐藏起来,即可变成下面的形式:

@sellOrder
Order(Taste: bayberry, Tea: Oolang, sweetness: halfSweetness, temperature: ice)@sellOrder
Order(Taste: bayberry, Tea: black, sweetness: halfSweetness, temperature: heat)
// 通过元编程的能力,上述代码在编译时将先转译成
let orderA = Order(Taste: bayberry, Tea: Oolang, sweetness: halfSweetness, temperature: ice)
orderA.sell() //元编程的能力将这句表达式折叠起来,靠宏定义将其展开
let orderB = Order(Taste: bayberry, Tea: black, sweetness: halfSweetness, temperature: heat)
orderB.sell()

这样店员就可以编写简单的脚本来和奶茶制作机以及大屏幕做交互,哪怕他从未学习过这门语言甚至连编程也没有接触过,通过简单的学习,他同样可以上手写出相应的订单脚本。因为 DSL 提供了足够高的抽象,抛除了语言本身的学习成本,并且脚本中所使用的词汇全部都是建立在我们之前设计的共通词汇之上。




3  DSL 的优势与发展


3.1 eDSL 优势与分类

在上述例子中,我们设计的是一门借助了宿主语言的 eDSL,由此我们也能看出当 DSL 借助宿主语言时的一些优势:


  • 最直观的一点,我们无需重新设计一门语言,这样就避免了语法设计和语义设计这种复杂的工作,我们要做的仅是利用宿主语言本身的特性来做抽象。

  • 正如上一条所提到的,我们可以利用宿主语言全部的语言特性。像例子中,我们就用到了 “枚举” 和“类”的语言类型,同时我们可以用到语言本身的类型系统或者语法糖等特性,并且可以直接借助语言本身互操作的能力与其他语言做交互,甚至可以借用元编程能力,使我们做更高一层的抽象,这都是 eDSL 本身的优势。当宿主语言特性足够强大丰富的时候,这也意味着相应的 eDSL 将有更多更灵活的功能,而且这些功能都是宿主语言 “免费” 提供的,eDSL 直接拿来使用即可。

  • 可以直接使用宿主语言提供的配套的工具,如 IDE 或者调试功能,无需从零开始投入到新语言的开发中。

所以 eDSL 相比外部 DSL 来说,开发的成本更低,并且更易推广,这也是开发者更喜欢使用 eDSL 的根本原因之一。在 eDSL 这一类别下,我们还可以分成不同的形态模式,下图展示一些不太严谨的 eDSL 的各种实现模式 [2]:


图片
图 8:eDSL 细化分类

 相比于内嵌式,生成式则是指领域抽象可以通过使用一些转译技术转译到宿主语言,如编译时宏、预处理等。


3.2 业界语言 eDSL 扩展能力分析

如上文所说,eDSL 最大的好处之一就是可以借用宿主语言本身的特性,所以现如今很多主流语言本身就为实现 eDSL 设计了很多优质的特性,使开发者可以构建面向特定领域的扩展。接下来本章节会对 Swift、Kotlin、Dart 以及 Scala 展开分析,看看他们有哪些特性有利于其扩展出以其为宿主语言的 eDSL。
Swift 的设计目标一直致力于 "Make Your Swift API Better",具体体现在有表现力、清晰无二义性且简单易用,这也正好是 DSL 所追逐的最终效果。关于 Swift 的 eDSL 分析如下:


图片
表 2:Swift 调研

 下面再来看看 17 年被 Google 官宣为安卓系统官方语言的 Kotlin,Kotlin 作为一个运行在 JVM 上兼容 Java 的静态语言,其语法更简洁,且兼具了很多实用功能,具体如下:


图片
表 3:Kotlin 调研

目标成为下一代结构化 Web 开发语言,Google 主导开发的 Dart 语言也同样很易于扩展 eDSL:


图片
表 4:Dart 调研

 Scala 作为一门拥有强大类型系统和类型约束的语言,结合其本身简洁的语法设计,使得依赖类型化抽象能力实现的 eDSL 拥有较强的领域表现能力:


图片
表 5:Scala 调研

 从上述四门语言可以看出,易于实现 eDSL 的语言通常都具有以下特点:


  • 支持面向对象:可以用类或者接口等抽象映射出领域内专有的实体,甚至有时候会提供一些类型扩展的机制来对于无法修改的类型做扩展。

  • 元编程能力:同时我们可以发现以上四门语言都具备元编程能力,元编程能力可以将复杂的实现隐藏起来,使面对用户的语法保持简洁并且精炼,并且支持用户自定义语义,如利用宏或注解等编译时元编程能力创造出一种不同于宿主语言的语法。

  • 声明式范式:声明式范式完美契合 DSL 的理念,因为用户不用关心怎么做,他只需要关心自己要什么即可。为了达到这一功能,链式调用、宏、类型扩展等特性发挥了重大作用。

  • 语法简洁:为了减少宿主语言本身语法的噪音,语法糖是一种有效的手段,比如一些标点的省略,关键字的省略以及简化的 Lambda 或是尾随闭包。

接下来我们将结合具体的 eDSL 实例来看看其相比外部 DSL 的优势与收益。


3.3  UI 领域的 eDSL —— SwiftUI

SwiftUI 是一门基于 Swift 构建的声明式 UI,这当然也是一门针对 UI 领域的 eDSL,下面是一段 SwiftUI 官网提供的代码 [6]:

import SwiftUI
Struct Content: View {@State var model = Themes.listModelvar body: some View {List(model.items, action: model.selectItem) { item inImage(item.image)VStack(alignment: .leading) {Text(item.title)Text(item.subtitle).color(.gray)}}}
}

当我们看到代码的时候,其实我们已经很容易的想像出对应的 UI 的布局,这便是声明式 UI 范式所带来的最大的收益。而为了实现这一范式,SwiftUI 借助了许多宿主语言本身的能力,这也正是 eDSL 的优势所在,通过这段代码我们可以看出上一章节所提到的所有四个特性:


  • 首先,它将组件等实体封装成为一个个对象,并且这些对象的命名都在前端领域的统一词汇中。这样当用户需要一个组件的时候,只需要将对应的组件实例化出来即可,这就加强了模型本身与用户的沟通能力,也是其为了实现 "UI as Code" 效果最根本的一步;

  • 再者,它通过注解的能力对其复杂的状态管理相关的逻辑代码做了隐藏,这段被隐藏的复杂逻辑在本文不做展开,有兴趣的读者可以去了解一下 SwiftUI 的 MVVM 模型以及其观察者模式;

  • 接下来,为了达到其声明式范式的效果,SwiftUI 借用了宿主语言命名参数以及链式调用的特性,使其达到用户可以通过 [组件名].[属性名].[属性名] 的形式绘制组件的效果。用户无需关心我如何去画这个组件,用户关心的就是我需要组件有哪些属性即可,这也正是上文提到的 "What";

  • 最后,SwiftUI 利用语法糖,如省略关键字 new 和 return,或者简化的枚举(比如例子中的 ".leading" 和 ".gray" )来消减语法的噪音。其还利用类型系统,如支持不透明返回类型,通过靠编译器本身的推导来简化开发者手写复杂的返回类型。

同时 SwiftUI 可以使用 Swift 本身的开发工具如 IDE 等。正是得益于这些现代编程语言相关特性的发展,才使我们在现实的软件开发中见证了 DSL 所具备的潜力。




4  总结


4.1 小结

本文到此就已接近尾声,回顾一下本文所有的内容,我们认识到了 DSL 是一种极其友好的领域建模方式,这得力于其强大的描述建模领域的表达能力。随着现代编程语言的发展,在语言层面对于 DSL 设计的支持越来越充分,比如借助高阶函数的能力,使用户在能够准确表达特定领域中的名词基础上,还能够很好地仿效该领域的动词 [2]。

元编程的能力以及灵活的类型系统,更是打开了宿主语言本身表现力的限制,这里引用 Debasish Ghosh 说过的一句话 [2]:对于一种能力充分的语言来说,限制其表现力的只有使用者的创造力而已。

总结最近发展起来的语言,不难发现众多语言的元编程的能力都在不断提高,这也正是各个通用语言为了实现 eDSL 所作的努力,所以元编程的发展对于 eDSL 的表现力与创造力是至关重要的一环,如果读者想了解有关元编程相关的细节,可以时刻关注 SIG-元编程 发表的相关文章,相信读者一定会大有收获。


4.2 未来的展望

像上述列举的 Scala、Kotlin、Swift 和 Dart 在 DSL 的发展上都有不同程度的布局,基于这些通用语言来看,类型系统以及元编程能力的发展,对于 DSL 未来的发展将会是挑战之一,同时如何最大程度消减宿主语言带入的噪音(如报错信息不够领域化、无关领域的类型信息)也是我们需要考虑的关键问题之一,再者我们还需要思考下如何让宿主语言配套的调试或者 IDE 支持其扩展的 eDSL。

虽然仍有这么多挑战与问题值得我们去追寻,但是无论 DSL 如何发展,我们都需坚持其基本的设计原则,同时需多站在用户的角度来思考 DSL 未来的发展,这样 DSL 才会健康地成长。希望越来越多的软件开发者可以看见这一领域的价值与乐趣,并投身其中。

参考:

1. Domain-specific language - Wikipedia. (2021). Retrieved 28 June 2021, from https://en.wikipedia.org/wiki/Domain-specific_language

2. Ghosh, D. (2010). DSLs in action. Simon and Schuster.

3. Fowler, M. (2010). Domain-specific languages. Pearson Education.

4. 可画 (2021). Retrieved 28 June 2021, from https://www.canva.cn/icons/MACF30PXThc/

5. Evans, E., & Evans, E. J. (2004). Domain-driven design: tackling complexity in the heart of software. Addison-Wesley Professional.

6. SwiftUI Overview - Xcode - Apple Developer. (2021). Retrieved 28 June 2021, from https://developer.apple.com/xcode/swiftui/

在这里插入图片描述

 


推荐阅读
  • 分享css中提升优先级属性!important的用法总结
    web前端|css教程css!importantweb前端-css教程本文分享css中提升优先级属性!important的用法总结微信门店展示源码,vscode如何管理站点,ubu ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 本文介绍了互联网思维中的三个段子,涵盖了餐饮行业、淘品牌和创业企业的案例。通过这些案例,探讨了互联网思维的九大分类和十九条法则。其中包括雕爷牛腩餐厅的成功经验,三只松鼠淘品牌的包装策略以及一家创业企业的销售额增长情况。这些案例展示了互联网思维在不同领域的应用和成功之道。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • NotSupportedException无法将类型“System.DateTime”强制转换为类型“System.Object”
    本文介绍了在使用LINQ to Entities时出现的NotSupportedException异常,该异常是由于无法将类型“System.DateTime”强制转换为类型“System.Object”所导致的。同时还介绍了相关的错误信息和解决方法。 ... [详细]
  • express工程中的json调用方法
    本文介绍了在express工程中如何调用json数据,包括建立app.js文件、创建数据接口以及获取全部数据和typeid为1的数据的方法。 ... [详细]
author-avatar
惯性hold不住
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有