1. Introduction
今天,我们将讨论一个被称为“domain-specific languages”的概念,特别是“embedded domain-specific languages”。这听起来可能是一个行话术语,但它肯定是您遇到过的东西,您甚至可以每天使用它。
在给出前期定义以便我们都了解什么是特定于域的语言后,我们将在Swift中创建一个,并逐步为其添加越来越多的高级功能。这是一个玩具的例子,但它包含许多核心想法,玩起来会很有趣。
2. Definitions
首先,我们来了解一些定义。“特定域语言”,简称DSL,是为服务特定域而构建的语言。这与Swift等语言形成鲜明对比,Swift是一种非常通用的语言,能够制作iOS应用程序、服务器端应用程序、CLI工具、脚本等。
如果您以前写过数据库SQL查询,那么您使用的是DSL。SQL是一个DSL,用于描述如何与数据库交互:
SELECT id, name
FROM users
WHERE email = 'blob@pointfree.co'
此外,如果您曾经编写过HTML,那么您使用的是DSL。HTML是一个DSL,用于描述如何布局文档:
<html>
<body>
<p>Hello World!</p>
</body>
</html>
此外,如果您曾经使用过CocoaPods或Carthage,那么您使用的是DSL。
// Cocoapods
platform :ios, '8.0'
use_frameworks!
target 'MyApp' do
pod 'NonEmpty', '~> 0.2'
pod 'Overture', '~> 0.1'
end
// Carthage
github "pointfreeco/NonEmpty" ~> 0.2
github "pointfreeco/Overture" ~> 0.1
您不一定用这些语言进行任何应用程序,但它们都很好地服务于他们的域,这正是它们被称为“特定领域语言”的原因。
一旦你理解了DSL的概念,这是理解“嵌入式特定领域语言”(简称EDSL)的一小步。这些是嵌入在其他语言中的DSL。在上述示例中,SQL、HTML和Carthage都是非嵌入式DSL。因为从技术上讲,Podfile是一个Ruby文件,您编写诚实的Ruby代码来描述您的依赖项。
因此,这是我们今天将讨论的两种DSL口味。我们已熟悉DSL,这是一个特定域语言的广泛概念,我们也熟悉了嵌入式DSLS,这些DSLS是托管在Ruby等另一种语言或我们今天将使用的DSL:Swift。
3. An arithmetic expression DSL
让我们从一个非常著名的DSL玩具示例开始:建模一种可以表示一些非常简单的算术运算的表达式类型。
考虑只允许整数和加法的算术表达式:
3 + (4 + 5)
(3 + 4) + 5
构成这个表达式的单位是什么?这个表达式的特定部分似乎要么是整数文字,要么是两个表达式的总和+。这直接转化为枚举描述:
enum Expr {
case int(Int)
indirect case add(Expr, Expr)
}
请注意,此枚举是递归的,因为加法可以将可以构造的任何其他两个表达式加在一起。
我们可以很容易地构造这个表达式的值:
Expr.int(3)
Expr.add(.int(3), .int(4))
Expr.add(.add(.int(3), .int(4)), .int(5))
Expr.add(.add(.int(3), .int(4)), .add(.int(5), .int(6)))
如果我们添加大量的换行符和缩进,我们将看到我们正在制作一个树状结构:
Expr.add(
.add(
.int(2),
.int(3)
),
.add(
.int(4),
.int(5)
)
)
因此,我们现在提升了所有仅涉及整数和加号的算术表达式集未Swift类型和值。
让我们也快速使Expr符合ExpressibleByIntegerLiteral,以便更容易构建这些值:
extension Expr: ExpressibleByIntegerLiteral {
init(integerLiteral value: Int) {
self = .int(value)
}
}
Expr.add(.add(2, 3), .add(4, 5))
好吧,这很酷:我们开始研究仅涉及整数和加法的算术表达式,并且能够通过将其直接转换为枚举来描述这些表达式的所有部分,然后使用该枚举,我们可以制作表示加法和整数的Swift值,而无需内联执行这些表达式。
我们能用这个东西做什么?我们可以开始为它编写evaluators。例如,如果我们想计算一个表达式来计算它所代表的最终整数,该怎么办:
func eval(_ expr: Expr) -> Int {
switch expr {
case let .int(value):
return value
case let .add(lhs, rhs):
return eval(lhs) + eval(rhs)
}
}
现在我们可以评估我们上面定义的表达式
eval(.add(.add(3, 4), .add(5, 6)))
// 18
好的,那很有趣。但除了纯数字评估外,我们还可以定义对这种DSL的解释。例如,我们可以将表达式打印到字符串中:
func print(_ expr: Expr) -> String {
switch expr {
case let .int(value):
return "\(value)"
case let .add(lhs, rhs):
return "\(print(lhs)) + \(print(rhs))"
}
}
这是一个非常天真的打印,但它有效:
print(.add(.add(2, 3), .add(4, 5)))
// "3 + 4 + 5 + 6"
我们现在有两种不同的方法来评估或解释这个DSL:我们可以将其完全计算成它所代表的整数,或者我们可以将其渲染成一个可以显示给用户的字符串。
DSL就是这样发展的。您定义DSL的数据类型表示形式,然后定义将其映射到不同表示的不同方式。在这种情况下,我们将算术表达式映射到整数和字符串,具体取决于我们是否要计算或打印。
4. Adding multiplication to our DSL
这种表达式类型现在非常简单,所以让我们试着通过支持更多的算术运算来加强它。让我们尝试乘法。
让我们以我们最初的例子为例,让它复杂化。
3 * (4 + 5)
(3 * 4) + 5
现在我们要么有一个整数,一个加号,要么一个时间要处理。
这又是一个需要添加到我们原始数据类型的案例。
enum Expr {
case int(Int)
indirect case add(Expr, Expr)
indirect case mul(Expr, Expr)
}
我们收到几个编译器错误,所以让我们修复它们,看看我们还剩下什么。首先,让我们修复eval函数。
func eval(_ expr: Expr) -> Int {
switch expr {
case let .int(value):
return value
case let .add(lhs, rhs):
return eval(lhs) + eval(rhs)
case let .mul(lhs, rhs):
return eval(lhs) * eval(rhs)
}
}
我们还需要更新print函数。
func print(_ expr: Expr) -> String {
switch expr {
case let .int(value):
return "\(value)"
case let .add(lhs, rhs):
return "\(print(lhs)) + \(print(rhs))"
case let .mul(lhs, rhs):
return "\(print(lhs)) * \(print(rhs))"
}
}
让我们通过调整我们刚才评估的表达式来进行旋转:
eval(.mul(.add(3, 4), .add(5, 6)))
// 77
准确计算出来了,看起来不错。
让我们对print做同样的事情:
print(.mul(.add(3, 4), .add(5, 6)))
// 3 + 4 * 5 + 6
啊,好吧,这不太对。我们真的想要(2 + 3)*(4 + 5)。事实证明,我们的打印功能不太正确。
要修复它,我们需要添加一些括号:
func print(_ expr: Expr) -> String {
switch expr {
case let .add(lhs, rhs):
return "(\(print(lhs)) + \(print(rhs)))"
case let .int(value):
return "\(value)"
case let .mul(lhs, rhs):
return "(\(print(lhs)) * \(print(rhs)))"
}
}
print(.mul(.add(2, 3), .add(4, 5)))
// "((3 + 4) * (5 + 6))"
很好,现在我们的print输出的字符串在算术上是正确的。
5. Transforming our DSL
将这种DSL作为简单的Swift类型感觉真的很好。我们可以在类型级别为它添加功能,Swift确保我们在每个evaluator和interpreter中处理此功能。
接下来我们可以用DSL做什么?好吧,因为它只是一种Swift数据类型,我们可以在非常高的水平上转换它!就像我们转换数组和字典一样,我们可以转换DSL,而不必担心eval、print或其他功能。
让我们考虑一些愚蠢的事情,只是为了让我们热身:让我们将所有加法实例换成乘法,反之亦然:
func swap(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(lhs, rhs):
return .mul(lhs, rhs)
case let .mul(lhs, rhs):
return .add(lhs, rhs)
}
}
现在,让我们用一个表达方式来交换它。
这看起来不太对。虽然外部乘法已换成加法,但内部加法没有换成乘法。
我们忘了进一步交换嵌套表达式。
func swap(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(lhs, rhs):
return .mul(swap(lhs), swap(rhs))
case let .mul(lhs, rhs):
return .add(swap(lhs), swap(rhs))
}
}
print(swap(.mul(.add(3, 4), .add(5, 6))))
// "((3 * 4) + (5 * 6))"
现在它起作用了:我们深入所有子表达式并交换了它们。
这是一个愚蠢的例子,所以让我们尝试一些更有力量的东西。
让我们考虑一些更严肃的事情:优化。现在很容易建立可以相当简化的表达式了。例如,这个表达式:
print(Expr.add(.mul(2, 3), .mul(2, 4)))
// ((2 * 3) + (2 * 4))
在数字上等同于此:
print(.mul(2, .add(3, 4)))
// (2 * (3 + 4))
因为我们将这些表达式描述为Swift值,所以抽象地执行此转换。
让我们尝试将此优化作为DSL的转换来实现。
func simplify(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(.mul(a, b), .mul(c, d)) where a == c:
case .mult:
return expr
}
}
现在,where子句要求我们使Expr equatable,但Swift免费给了我们。
enum Expr: Equatable {
我们现在可以把它变成我们正在寻找的形状。
func simplify(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(.mul(a, b), .mul(c, d)) where a == c:
return .mul(a, .add(b, d))
case .mult:
return expr
}
}
我们的switch需要exhaustive,所以让我们传入所有其他add案例。
func simplify(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(.mul(a, b), .mul(c, d)) where a == c:
return .mul(a, .add(b, d))
case .add:
return expr
case .mult:
return expr
}
}
让我们试一试:
print(simplify(Expr.add(.mul(2, 3), .mul(2, 4))))
// (2 * (3 + 4))
太棒了,它奏效了!我们使用 Swift 强大的模式匹配来推动整个过程。在switch中,我们字面上匹配了添加两个乘法的形状,在这种情况下,我们被允许将4个项简化为仅3个。
对于这个特定的DSL来说,这似乎不是一个巨大的胜利,因为eval和print功能非常简单。然而,有一些DSL描述了非常昂贵的操作,在这种情况下,以高层次的方式简化DSL值可能会节省相当多的工作。
它的功能甚至可以改进。一个简单的胜利是识别形式a * c + b * c的模式,并将其分解为(a + b)* c。我们也可以将1 *任何东西简化为任何东西,并折叠该乘法。
6. Adding a variable to our DSL
让我们在DSL中添加一个高级功能,看看它如何改变我们迄今为止所做的所有工作。我们将添加引入变量的能力,该变量的值在创建DSL值时未知,但仅在尝试eval DSL时。让我们用我们的示例表达式来描述它。
//x * (4 + 5)
//(x * 4) + 5
这允许我们创建参数化的算术表达式,我们可以稍后提供这些数据。让我们从更改DSL枚举开始:
enum Expr: Equatable {
case int(Int)
indirect case add(Expr, Expr)
indirect case mul(Expr, Expr)
case `var`
}
好吧,这打破了我们现有的很多代码。eval, print, swap and simplify函数都需要修复。看看我们需要做些什么来修复它们。
对于eval,我们必须确定评估变量var意味着什么:
func eval(_ expr: Expr) -> Int {
switch expr {
case let .int(value):
return value
case let .add(lhs, rhs):
return eval(lhs) + eval(rhs)
case let .mul(lhs, rhs):
return eval(lhs) * eval(rhs)
case let .var:
}
}
我们需要返回一个整数,但我们没有整数可以使用。我们能做什么?好吧,我们需要调整eval函数,以便我们被迫在特定值下计算表达式。这意味着我们需要更改签名以获得额外的参数:
func eval(_ expr: Expr, with value: Int) -> Int {
switch expr {
case let .int(value):
return value
case let .add(lhs, rhs):
return eval(lhs, with: value) + eval(rhs, with: value)
case let .mul(lhs, rhs):
return eval(lhs, with: value) * eval(rhs, with: value)
case let .var:
return value
}
}
让我们也更新print。要打印这个新表达式,我们必须知道如何将变量打印到字符串中。好吧,我们可以自由选择任何我们想要的东西。我们可以使用问号?,像unknown这样的单词,像**_**这样的符号或任何东西。但是,我喜欢数学,数学喜欢x,所以让我们用它来:
func print(_ expr: Expr) -> String {
switch expr {
case let .int(value):
return "\(value)"
case let .add(lhs, rhs):
return "(\(print(lhs)) + \(print(rhs)))"
case let .mul(lhs, rhs):
return "\(print(lhs)) * \(print(rhs))"
case .var:
return "x"
}
}
print(.add(.var, 2))
// "x + 2"
swap函数只想交换加法和乘法,因此它真的不需要对变量做任何事情:
func swap(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(lhs, rhs):
return .mul(swap(lhs), swap(rhs))
case let .mul(lhs, rhs):
return .add(swap(lhs), swap(rhs))
case .var:
return expr
}
}
最后更新simplify。没有办法进一步简化单个变量:
func simplify(_ expr: Expr) -> Expr {
switch expr {
case let .add(.mul(a, b), .mul(c, d)) where a == c:
return simplify(.mul(a, .add(b, d)))
case .int:
return expr
case .add:
return expr
case .mult:
return expr
case .var:
return expr
}
}
现在编译器很高兴,我们可以玩弄我们的新表达式了。
从eval开始,我们可以用var替换int。
eval(.mul(.add(.var, 4), .add(5, 6)), with: 3)
// 77
print怎么样?当我们用var替换int时,我们可以看到变量是如何打印的。
print(.mult(.add(3, .var), .add(5, 6)))
// "((3 + x) * (5 + 6))"
我们可以在整个过程中不断添加vars,并按我们的预期打印内容。
print(.mult(.add(3, .var), .add(5, .var)))
// "((3 + x) * (5 + x))"
让我们确保simplify仍然适用于我们的vars。
print(simplify(Expr.add(.mul(.var, 3), .mul(.var, 4))))
// (x * (3 + 4))
这是一个非常强大的想法:拥有高度可转换的数据类型,然后具有各种解释,当数据类型发生变化时,我们可以以小的方式进行修改,以保持工作。
在函数式编程社区中,这是一个众所周知的想法:DSL-解释器模式。您创建DSL,并将其与任何解释问题完全分开,然后您可以用在各种解释中,如eval和print。这是一种非常有趣的方式来消除这些担忧。DSL在Swift社区中还不是超级常见,也不是Swift中解决问题的常见思维方式。但是,令人惊讶的是,我们在日常编程生活中遇到的许多问题可以重塑为创建DSL的想法,以纯粹面向数据、无副作用的方式建模域问题,然后提供解释,以各种方式评估该DSL。
下次,我们将在这个DSL中添加两个高级功能,这可能看起来有点令人惊讶,我们最终会回答:“这有什么意义?”