Video #26: Domain‑Specific Languages: Part 1
Episode: Video #26 Date: Aug 20, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep26-domain-specific-languages-part-1

Description
We interact with domain-specific languages on a daily basis, but what does it take to build your own? After introducing the topic, we will begin building a toy example directly in Swift, which will set the foundation for a future DSL with far-reaching applications.
Video
Cloudflare Stream video ID: 819d90cea7d99ed877838a60fee897b4 Local file: video_26_domain-specific-languages-part-1.mp4 *(download with --video 26)*
Transcript
— 0:05
Today we are going to talk about a concept known as “domain-specific languages”, and in particular “embedded domain-specific languages”. It may sound like a jargony term, but it’s something that you have definitely come across and you may even use it on a daily basis.
— 0:23
After giving the upfront definitions so that we all understand what a domain-specific language is, we will create one right in Swift and progressively add more and more advanced features to it. It’s a toy example, but it contains a lot of the core ideas and it can be a lot of fun to play with. Definitions
— 0:42
Let’s start by getting some definitions out of the way. A “domain-specific language”, or DSL for short, is a language that is built to serve a particular domain. This is in contrast to languages like Swift, which is a very general purpose language, capable of doing iOS apps, server side apps, CLI tools, scripting and more.
— 1:12
If you’ve ever written a database SQL query before, you were using a DSL. SQL is a DSL for describing how one interacts with a database: SELECT id, name FROM users WHERE email = ' [email protected] '
— 1:32
It does that one thing, and it does it very well.
— 1:38
Also, if you’ve ever written HTML, you were using a DSL. HTML is a DSL for describing how to lay out documents: <html> <body> <p>Hello World!</p> </body> </html>
— 1:51
Further, if you’ve ever used CocoaPods or Carthage you were using a 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
— 2:19
You can’t necessarily make any application in these languages, but they all serve their domain very well, which is precisely why they’re called “domain-specific languages”.
— 2:35
Once you understand the concept of a DSL, it’s a small step to understanding “embedded domain specific languages”, or EDSL for short. These are DSLs that are embedded in some other language. In the above examples, SQL, HTML and Carthage are all non-embedded DSL. They are languages unto themselves. However, CocoaPods is embedded, because technically the Podfile is a Ruby file, and you write honest Ruby code in order to describe your dependencies.
— 3:11
So these are the two flavors of DSLs we’ll be talking about today. We’ve got DSLs, the broad concept of languages that are tuned to a particular domain, and we’ve got embedded DSLS, which are DSLs that are hosted in another language, like Ruby, or what we’ll be using today: Swift. An arithmetic expression DSL
— 3:30
Let’s start with a very famous toy example of a DSL: modeling an expression type that can represent some very simple arithmetic operations.
— 3:43
Consider the arithmetic expressions that only allow integers and addition: 3 + (4 + 5) (3 + 4) + 5
— 3:48
What are the units that make up this expression? It seems that a particular part of this expression is either an integer literal, or the sum + of two expressions. This translates directly into an enum description: enum Expr { case int(Int) indirect case add(Expr, Expr) } Take note that this enum is recursive, because the add case can add together any other two expressions that can be constructed.
— 4:24
We can construct values of this expression quite easily: 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)))
— 5:11
If we add copious amounts of newlines and indentation we will see we are making a tree-like structure: Expr.add( .add( .int(2), .int(3) ), .add( .int(4), .int(5) ) )
— 5:27
And so we have now lifted the set of all arithmetic expressions only involving integers and pluses up to Swift types and values.
— 5:36
Let’s also quickly make Expr conform to ExpressibleByIntegerLiteral in order to make it a lil easier to construct these values: extension Expr: ExpressibleByIntegerLiteral { init(integerLiteral value: Int) { self = .int(value) } } Expr.add(.add(2, 3), .add(4, 5)) That’s a lil nicer.
— 6:16
Alright this is cool: we set out to look at arithmetic expressions that only involved integers and addition and were able to describe all the parts of those expressions by translating it directly into an enum, and then using that enum we could make Swift values that represent additions and integers without executing these expressions inline.
— 6:41
What can do we do with this thing? We can start writing evaluators for it. For example, what if we wanted to evaluate an expression to just compute the final integer it represents: func eval(_ expr: Expr) -> Int { switch expr { case let .int(value): return value case let .add(lhs, rhs): return eval(lhs) + eval(rhs) } }
— 7:42
And now we get to evaluate the expression we defined above: eval(.add(.add(3, 4), .add(5, 6))) // 18
— 7:54
OK, that’s fun. But we can also define interpretations of this DSL other than just plain numeric evaluation. For example, we could print the expression to a string: func print(_ expr: Expr) -> String { switch expr { case let .int(value): return "\(value)" case let .add(lhs, rhs): return "\(print(lhs)) + \(print(rhs))" } }
— 8:56
It’s a very naive printing, but it works: print(.add(.add(2, 3), .add(4, 5))) // "3 + 4 + 5 + 6"
— 9:05
We now have two different ways of evaluating, or interpreting, this DSL: we can completely compute it out to the integer it represents, or we can render it out to a string that’s displayable to a user.
— 9:22
This is kinda how things go with DSLs. You define your data type representation of your DSL, and then you define different ways of mapping it to different representations. In this case we’ve mapped arithmetic expressions to both integers and strings depending on if we want to evaluate or print. Adding multiplication to our DSL
— 9:38
This expression type is pretty simple right now, so let’s try to beef it up a bit by supporting even more arithmetic operations. Let’s try multiplication.
— 9:51
Let’s take our original example and complicate it. 3 * (4 + 5) (3 * 4) + 5 Now we have either an integer, a plus, or a times to handle.
— 10:04
That’s one more case to add to our original data type. enum Expr { case int(Int) indirect case add(Expr, Expr) indirect case mul(Expr, Expr) }
— 10:15
We get a couple of compiler errors, so let’s fix em, and see what we are left with. First, let’s fix 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) } }
— 10:42
We also need to update 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))" } }
— 11:01
Let’s take it for a spin by tweaking the expression we evaluated a moment ago: eval(.mul(.add(3, 4), .add(5, 6))) // 77
— 11:10
The math checks out, so looking good.
— 11:16
Let’s do the same for printing: print(.mul(.add(3, 4), .add(5, 6))) // 3 + 4 * 5 + 6
— 11:22
Ah, ok this isn’t quite right. See, multiplication has a higher precedence than multiplication, so this expression really represents 2 + (3 * 4) + 5 , but we really wanted (2 + 3) * (4 + 5) . Turns out our print function isn’t quite right.
— 11:44
To fix it, we need to add some parentheses: 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))"
— 11:57
Nice, now our printer outputs a string that is arithmetically correct. Transforming our DSL
— 12:02
Having this DSL as as simple Swift type is feeling really nice. We can add features to it at the type level and Swift makes sure we handle this feature in each evaluator and interpreter.
— 12:20
What’s the next thing we can do with DSLs? Well, because it’s just a Swift data type, we can transform it at a very high level! Just as we transform arrays and dictionaries, we can transform the DSL without worrying about eval , print , or other functions.
— 12:49
Let’s consider something kinda silly just to get us warmed up: let’s swap all instances of addition for multiplication and vice versa: 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) } }
— 13:34
Now, let’s take one of our expressions and swap it. print(swap(.mul(.add(3, 4), .add(5, 6)))) // "((3 + 4) + (5 + 6))"
— 13:44
This doesn’t look quite right. While the outer multiplication has been swapped out for addition, the inner addition wasn’t swapped out for multiplication.
— 13:56
We forgot to further swap the nested expressions. 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))"
— 14:10
Now it works: we dove into all of the subexpressions and swapped them out.
— 14:18
It’s a silly example, so let’s try something with a little more power.
— 14:24
Let’s consider something more serious: optimization. Right now it’s easy to build up expressions that could be simplified quite a bit. For example this expression: print(Expr.add(.mul(2, 3), .mul(2, 4))) // ((2 * 3) + (2 * 4))
— 14:48
Is numerically equivalent to this: print(.mul(2, .add(3, 4))) // (2 * (3 + 4))
— 15:06
Because we describe these expressions as a Swift value, perform this transformation abstractly.
— 15:16
Let’s try to implement this optimization as a transformation on the 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 } } Now the where clause requires us to make Expr equatable, but Swift gives that to us for free. enum Expr: Equatable {
— 16:50
We can now transform it into the shape we’re looking for. 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 } } Our switch needs to be exhaustive, so let’s pass all other add cases through. 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 } }
— 17:29
Let’s give it a spin: print(simplify(Expr.add(.mul(2, 3), .mul(2, 4)))) // (2 * (3 + 4))
— 17:42
Amazing, it worked! And we used Swift’s powerful pattern matching to drive the whole thing. In the switch we literally pattern matched on the shape of adding the addition of two multiplications, and in that single case we were allowed to reduce 4 terms into just 3.
— 18:23
And it may not seem like a huge win for this particular DSL because the eval and print functions are quite simple. However, there are DSLs out there that describe very expensive operations, and in those cases having a high level way of simplifying DSL values can potentially save quite a bit of work.
— 18:52
The function could even be improved. One easy win would be to also recognize patterns of the form a * c + b * c and factorize it to be (a + b) * c . We could also simplify 1 * anything to be anything and collapse that multiplication. But we’ll save these for exercises. Adding a variable to our DSL
— 19:13
Let’s add an advanced feature to our DSL and see how it changes all of the work we have done so far. We will add the ability to introduce a variable whose values are not known at the moment of creating values of the DSL, but only when trying to evaluate the DSL. Let’s describe it in our example expression. //x * (4 + 5) //(x * 4) + 5
— 19:41
This allows us to create arithmetic expressions that are parameterized, and we can supply that data at a later date. Let’s start by altering the DSL enum: enum Expr: Equatable { case int(Int) indirect case add(Expr, Expr) indirect case mul(Expr, Expr) case var }
— 20:04
OK, this broke a lot of our existing code. The eval , print , swap and simplify function all need to be fixed. Let’s go through this one at a time and see what we need to do to fix them.
— 20:08
For eval we have to determine what it means to evaluate the variable case: 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: } }
— 20:23
We need to return an integer, but we don’t have an integer to use. What can we do? Well, we need to adapt the eval function so that we are forced to evaluate an expression at a particular value. This means we need to change the signature to take an extra param: 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 } }
— 20:44
Let’s also update print . To print this new expression we have to know how to print a variable to a string. Well, we are free to choose anything we want. We could use a question mark ? , a word like unknown , a symbol like _ or anything. But, I like math, and math likes x , so let’s use that: 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"
— 21:04
The swap function only wanted to swap addition and multiplication, so it doesn’t really need to do anything with variables: 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 } }
— 21:13
And finally simplify . There’s no way to further simplify a single variable, so that also just passes through: 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 } }
— 21:19
Now that the compiler is happy, we can play around with our new expression.
— 21:23
Starting with eval , we can take a line from earlier and replace an int with a var . eval(.mul(.add(.var, 4), .add(5, 6)), with: 3) // 77
— 21:45
How about print ? When we replace an int with var , we can see how a variable is printed. print(.mult(.add(3, .var), .add(5, 6))) // "((3 + x) * (5 + 6))"
— 21:54
We can keep adding var s throughout and things print as we expect. print(.mult(.add(3, .var), .add(5, .var))) // "((3 + x) * (5 + x))"
— 22:01
Let’s make sure that simplify still works with our var s. print(simplify(Expr.add(.mul(.var, 3), .mul(.var, 4)))) // (x * (3 + 4))
— 22:12
And there we have it! Twice now we’ve seen that we can merely add a case to our type, and the compiler walks us through all the changes we need to make to existing code we’ve written. To be continued…
— 22:31
It’s a really powerful idea: having a data type that’s highly transformable and then having various interpretations that, when that data type changes, we can modify in small ways to keep things working.
— 22:49
In the functional programming community this is a well-known idea: the DSL-interpreter pattern. You create your DSL and fully separate it from any concerns of interpretation, and then you can sprinkle in various interpretations, like eval and print . It’s a very interesting way of separating out these concerns
— 23:23
We’re going to stop here because we want to take it a little slowly. DSLs aren’t yet super common in the Swift community, and it’s not a common way of thinking through problems in Swift. Still, it may be surprising just how many of the problems we encounter in our day-to-day programming lives can be recast into the idea of creating a DSL to model a domain problem in a purely data oriented, side effect-free way, and then providing interpretations to evaluate that DSL in a variety of ways.
— 23:56
Next week, we’ll add two advanced features to this DSL that may seem kinda surprising, and we will finally answer: “what’s the point?” Downloads Sample code 0026-edsls-pt1 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .