# Domain‑Specific Languages: Part 1

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中添加两个高级功能,这可能看起来有点令人惊讶,我们最终会回答:“这有什么意义?”