devxlogo

Introduction to Internal DSLs in Scala

Introduction to Internal DSLs in Scala

Introduction

A domain-specific language (DSL) is a language that you design with a specific domain in mind. There are two types of domain-specific languages (DSLs): external and internal. The former implicates???in the simplest case???parsing a string into the programming language syntax. The latter, on the other hand, involves implementing a language within your own programming language. Quoting Martin Fowler, “Internal DSLs are particular ways of using a host language to give the host language the feel of a particular language”.

Note that, up until now, I didn’t refer to Scala at all because a DSL is a language-agnostic concept and you can implement it in any language of choice. This is particularly true for external DSLs. However, when it comes to internal DSLs, if the programming language syntax is not flexible enough, you’ll have a hard time implementing them. Fortunately Scala is a DSL-friendly language so it’s of great help in developing ad-hoc internal DSLs.

In this article, I’ll show you how to design and implement a simple internal DSL. From now on, when I use the term DSL I’ll be referring to an internal DSL, unless otherwise specified.

Case Study

As a case study, consider the currency domain. Firstly I’ll implement it as a classic API and then I will transform it into a DSL so you can see what it takes to turn a regular?API into a more expressive domain-specific language.

I’ll start by defining the Currency:

trait Currency {  def getCode: String}object Currency {  object USD extends Currency {    val getCode: String = "USD"  }  object EUR extends Currency {    val getCode: String = "EUR"  }  object GBP extends Currency {    val getCode: String = "GBP"  }  def apply(s: String): Currency = s.toUpperCase match {    case "USD" => USD    case "EUR" => EUR    case "GBP" => GBP  }}

OK, nothing to write home about. A simple trait with its companion object whose apply method takes a string and instantiates a Currency?object by pattern matching on the given string.

At this point I need a type that lets me get the exchange rate from one currency to another. I don’t think a Map?would be a terrible choice here:

type Conversion = Map[(Currency, Currency), BigDecimal]

Here is an example of a currency map with real rates taken at the time this article was written:

val conversion: Conversion = Map(  (GBP, EUR) -> 1.39,  (EUR, USD) -> 1.08,  (GBP, USD) -> 1.5) 

That means 1 British pound equals 1.39 euros, 1 euro equals 1.08 US dollars and 1 British pound equals 1.5 US dollars.

This conversion map is fed into the Converter?class as follows:

case class Converter(conversion: Conversion) extends {  def convert(from: Currency, to: Currency): BigDecimal = {    if (from == to) 1    else conversion.getOrElse((from, to), 1 / conversion((to, from)))  }}

As you can see it has just one method, convert, that returns a conversion rate that is the value at the key (from, to)?if found, otherwise it looks for a reverse conversion, (to, from), and computes the rate using the reciprocal (1 / rate). It’s pretty intuitive if you think about it. If 1 euro equals 1.08 dollars, then 1 dollar equals 1/1.08 euros. This way the conversion map can be smaller, indeed you need to provide only one-way conversion and the convert?method will take care of the other way around. However if, for any reason, you need to provide two different conversions which do not follow the reciprocal rule, you can do it by passing in an ad-hoc conversion map.

Now we need a data structure to represent an amount plus its currency. For this purpose I used a case class, named Money, defined as follows:

case class Money(amount: BigDecimal, currency: Currency, converter: Converter) 

Obviously you would define some methods, within this class, in order to be able to perform operations among different instances. You would certainly implement the four basic operators (+, -, *, /) and the comparison methods such as >, < and so on:

def +(thatMoney: Money): Money =  performOperation(thatMoney, _ + _)def performOperation(thatMoney: Money, operation: (BigDecimal, BigDecimal) => BigDecimal): Money = {  thatMoney match {    case Money(v, c) if c == currency => Money(operation(amount, v), currency)    case Money(v, c) => performOperation(thatMoney.to(currency), operation)  }}def to(thatCurrency: Currency): Money = {  val rate = converter.convert(currency, thatCurrency)  Money(amount * rate, thatCurrency)}

As you can see the + operator is defined in terms of performOperation. This method converts thatMoney?to the same currency of the object it’s called upon only if the two currencies are not already the same. Afterward it computes the operation passed as a parameter, that, in this particular example, happens to be the + operation. The conversion is done within the to?method which, in turn, uses the converter to perform the actual conversion.

OK, now a usage example of this API:

val conversion: Conversion = Map(  (GBP, EUR) -> 1.39,  (EUR, USD) -> 1.08,  (GBP, USD) -> 1.50)val converter = Converter(conversion)// result is 79.8 USDval result = Money(42, USD, converter) + Money(35, EUR, converter) 

If you want you can convert it to GBP:

// resultToPound is 53.2 GBPval resultToPound = result.to(GBP) 

Up to now, nothing is new. However, wouldn’t it be nicer if you could write your expressions as follows instead?

val result = 42(USD) + 35(EUR)val resultToPound = result to GBP

I bet you prefer this last syntax. Well that is DSL and I’m going to transform the previous API in order to implement it.

Enter the Money DSL

Scala syntax has some important features that makes it a DSL-friendly language, the most important being implicits, the possibility to use operators as method names?and optional dots and parentheses?for arity-0 and arity-1 methods???that is zero-parameter and one-parameter methods, respectively. Explaining in details how implicits work in Scala is beyond the scope of this article. What you need to know here is that, thanks to implicits, you can extend existing classes and passing values to methods, well, implicitly.

So, in order to implement the Money DSL I need to redefine my Money class to pass the converter it needs implicitly:

case class Money(amount: BigDecimal, currency: Currency)(implicit converter: Converter) 

Furthermore, I need to extend the Int, Double?and BigDecimal?classes as follows:

implicit class BigDecimalOps(value: BigDecimal) {  def apply(currency: Currency)(implicit converter: Converter): Money = Money(value, currency)}implicit class IntOps(value: Int) {  def apply(currency: Currency)(implicit converter: Converter): Money = (value: BigDecimal).apply(currency)}implicit class DoubleOps(value: Double) {  def apply(currency: Currency)(implicit converter: Converter): Money = (value: BigDecimal).apply(currency)}

Basically, this extensions let me write expressions such as:

val dollars = 100(USD)val euros = 42.24(EUR) 

Remember that if a class has an apply?method then it can be called in two ways: x.apply(...)?and x(...). Both expressions end up invoking the apply method of the class of which x?is an instance.

What happens under the hood when the compiler encounters the expression 100(USD)? Put simply, it looks for an apply method in the Int?class. Since it won’t find one, it then checks if it is defined implicitly. You satisfy this need by importing the IntOps?class. At this point it creates an instance of Money?but it is able to do that only if there’s an implicit value of type Converter?in the context. For this purpose you just need to define such a value, for instance, as follows:

val conversion: Conversion = Map(  (GBP, EUR) -> 1.39,  (EUR, USD) -> 1.08,  (GBP, USD) -> 1.50)implicit val converter = Converter(conversion) 

The implicit?keyword does the trick both here and in the Money?constructor.

Add to this the optional dots and parentheses for arity-0 and arity-1 methods and you’ll be able to write expressions such as:

val result = 42(USD) + 35(EUR)val resultToPound = result to GBP

Which is equivalent to the less readable:

val result = 42(USD).+(35(EUR))val resultToPound = result.to(GBP) 

That’s all you need to turn a relatively boring API into a brilliant DSL.

Conclusion

If this is the first time you’ve met implicits in Scala, don’t worry if the concept is not completely clear, you’ll get used to. What’s important here is to get the big picture and you can then go through the details of each argument at your own pace.

In case you’re curious to see the whole Money DSL source code, you can find it on my GitHub account here: https://github.com/lambdista/money.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist