In the last episode we gave a defined “domain specific languages”, also known as DSLs, as any language that is highly tuned to a specific task. Some popular examples are SQL, HTML, and even Cocoapods and Carthage. We also defined an “embedded domain specific language”, also known as an EDSL, as a DSL that is embedded in an existing language. The Podfile
of Cocoapods is an example of this, because the Podfile
is technically written in standard Ruby code.
We then began constructing a toy EDSL example in Swift from first principles. It modeled an arithmetic expression where only integers, addition, multiplication, and variables were allowed. We defined two interpretations of this DSL: one for evaluating the expression to get an integer from it once you do all the arithmetic, and also a printer so that we could represent the expression as a string. We also experimented a bit with transforming the DSL abstractly, such as performing some basic simplification rules on an expression, such as factoring out common values.
This time we are going to add two very advanced features to our DSL: the ability to support multiple variables, and the ability to introduce let-bindings, which allows us to share expressions within our DSL. It’s kinda strange and cool.
Here’s all the code we wrote last time.
// x * (4 + 5)
// (x * 4) + 5
enum Expr {
case int(Int)
indirect case add(Expr1, Expr1)
indirect case mult(Expr1, Expr1)
case `var`(String)
}
extension Expr: ExpressibleByIntegerLiteral {
init(integerLiteral value: Int) {
self = .int(value)
}
}
func eval(_ expr: Expr, with value: Int) -> Int {
switch expr {
case let .int(value):
return value
case let .add(lhs, rhs):
return eval(lhs) + eval(rhs)
case let .mult(lhs, rhs):
return eval(lhs) * eval(rhs)
case .var:
return value
}
}
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 .mult(lhs, rhs):
return "(\(print(lhs)) * \(print(rhs)))"
case let .var:
return "x"
}
}
func simplify(_ expr: Expr) -> Expr {
switch expr {
case .int:
return expr
case let .add(.mult(a, b), .mult(c, d)) where a == c:
return .mult(a, .add(b, d))
case .add:
return expr
case .mult:
return expr
case .var
return expr
}
}
We first have an Expr
enum with cases describing different kinds of arithmetic expressions, including integers, addition, multiplication, and variables. We then have a few functions that interpret this expression type in various ways, including evaluation, printing, and simplification.