🎉 Black Friday Sale! Save 30% when you subscribe today.

Domain‑Specific Languages: Part 2

Episode #27 • Aug 27, 2018 • Subscriber-Only

We finish our introduction to DSLs by adding two new features to our toy example: support for multiple variables and support for let-bindings so that we can share subexpressions within a larger expression. With these fundamentals out of the way, we will be ready to tackle a real-world DSL soon!

Previous episode
Domain‑Specific Languages: Part 2
Next episode
Locked

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

Sign in with GitHub

Introduction

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.

Recap

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.


Downloads

Sample code

Get started with our free plan

Our free plan includes 1 subscriber-only episode of your choice, access to 64 free episodes with transcripts and code samples, and weekly updates from our newsletter.

View plans and pricing