Introducing DSLs in Scala

Domain specific language (DSL) is usually useful to simplify the interaction with a system by being applied to a small particular domain. They can be targeted to programmers by providing a simplified API to communicate with a system; or they may concern the so-called "business users" who may understand a domain well enough to create some scripts but are not programmers and could have difficulty dealing with a general-purpose programming language. There are, in general, two types of DSLs:

  • Internal DSLs
  • External DSLs

Observing internal DSLs

Internal DSLs use a host language (for instance, Scala) and the simplified usage is obtained by adding some syntactic sugar, through tricks and special constructs of the language. The book DSLs in Action by Debasish Ghosh illustrates the construction of Scala internal DSLs using features of the language such as infix notation and implicit conversions.

He has given the following DSL usage example that represents an executable program expressed in clear English: 200 discount bonds IBM for_client NOMURA on NYSE at 72.ccy(USD). Many transformations happen under the hood, but the business user is given a very clean syntax.

Such DSLs have the advantage that you are confident that you can express anything with them as the host language is of generic purpose (such as Scala). This means that sometimes you may be constrained to use a less clean syntax but you know you have the full power of the Scala compiler under your hands. Therefore, you will always succeed in producing a DSL script or program that implements the logic you want.

However, the full power of the compiler may also be something that you would like to avoid in many cases where you want to give your business user the possibility to only perform a few specific actions. For this purpose, you may implement external DSLs instead. There are a number of additional concerns including constrained syntax (for example, you can't avoid parentheses in some cases) and convoluted error messages.

Tackling external DSLs through parser combinators

An external DSL represents a domain language where the syntax is completely up to you. This means you can express things exactly the way you want to and can constrain your business users to only use specific words or meanings. This flexibility comes with a price of much more work to implement it as you need to define a grammar (typically Backus–Naur Form (BNF)), that is, define all the rules that apply to parse a meaning or script successfully. In Java, the task to write an external DSL can be cumbersome and it usually involves the ANTLR external framework.

In Scala, parser combinators are a notion very close to the definition of BNF grammars and can provide very concise and elegant code when writing external DSLs.

Once you get acquainted with a few particular operators to deal with the definition of the grammar, you will discover that writing an external DSL is fairly straightforward if your language is not too complex. A good source of information to learn all the symbols and operators involved in parser combinators is available at http://bitwalker.github.io/blog/2013/08/10/learn-by-example-scala-parser-combinators/.

The following experimental code illustrates a small DSL in the domain of finance consolidation where specific money accounts are manipulated as part of predefined formulae. The main method given at the end of the following snippet reflects a formula; for instance, you may parse the formula (3*#ACCOUNT1#) to construct an object-oriented structure that will be able to compute the result of multiplying the content of a given account by three:

package parsercombinator

import scala.util.parsing.combinator._
import java.text.SimpleDateFormat
import java.util.Date

object FormulaCalculator {
  
  abstract class Node
  
  case class Transaction(amount: Int)
  case class Account(name:String) extends Node {
    var transactions: Iterable[Transaction] = List.empty
  }
  
  def addNewTransaction(startingBalance: Int, t: Transaction) = startingBalance + t.amount
  def balance(account: Account) = account.transactions.foldLeft(0)(addNewTransaction)

  case class NumberOfPeriods (value: Int) extends Node {
    override def toString = value.toString
  }
  case class RelativePeriod (value:String) extends Node {
    override def toString = value
  }
  case class Variable(name : String) extends Node
  case class Number(value : Double) extends Node
  case class UnaryOp(operator : String, arg : Node) extends Node
  case class BinaryOp(operator : String, left : Node, right : Node) extends Node
  case class Function(name:String,arguments:List[Node]) extends Node {
    override def toString =
      name+arguments.mkString("(",",",")")
  }…

The objects that will result from the parsing of a formula are defined as case classes. Hence, continuing with the code:

…
  def evaluate(e : Node) : Double = {
    e match {
      case Number(x) => x
      case UnaryOp("-", x) => -(evaluate(x))
      case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))
      case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))
      case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))
      case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))
    }
  }

  object FormulaParser extends JavaTokenParsers {        
    
    val identifier: Parser[String] = ident
    val relative_period: Parser[RelativePeriod] = """([N|P|+|-][0-9]+|CURRENT)""".r ^^ RelativePeriod
    val number_of_periods: Parser[NumberOfPeriods] = """d+""".r ^^ (i => NumberOfPeriods(i.toInt))
    val account_name: Parser[String] = """[A-Za-z0-9_]+""".r
    
    def account: Parser[Account] = "#" ~> account_name <~ "#" ^^ { Account(_) }
    
    def function: Parser[Function] =
      identifier~"("~account~","~relative_period~","~number_of_periods~")" ^^ {
        case f~"("~acc~","~rp~","~nbp~")" => Function(f,List(acc,rp,nbp))
      } |
      identifier~"("~account~","~relative_period~")" ^^ {
        case f~"("~acc~","~rp~")" => Function(f,List(acc,rp))
      }

      def node: Parser[Node] =
        (term ~ "+" ~ term) ^^ { case lhs~plus~rhs => BinaryOp("+", lhs, rhs) } |
        (term ~ "-" ~ term) ^^ { case lhs~minus~rhs => BinaryOp("-", lhs, rhs) } |
        term

      def term: Parser[Node] =
        (factor ~ "*" ~ factor) ^^ { case lhs~times~rhs => BinaryOp("*", lhs, rhs) } |
        (factor ~ "/" ~ factor) ^^ { case lhs~div~rhs => BinaryOp("/", lhs, rhs) } |
        (factor ~ "^" ~ factor) ^^ { case lhs~exp~rhs => BinaryOp("^", lhs, rhs) } |
        factor

      def factor : Parser[Node] =
        "(" ~> node <~ ")" |
        floatingPointNumber ^^ {x => Number(x.toFloat) } |
        account |
        function

      def parse(text : String) =
            parseAll(node, text)        
    }

    // Parses 3 formula that make computations on accounts
    def main(args: Array[String]) {
        
        val formulaList = List("3*#ACCOUNT1#","#ACCOUNT1#- #ACCOUNT2#","AVERAGE_UNDER_PERIOD(#ACCOUNT4#,+1,12)")
        
        formulaList.foreach { formula =>
            val unspacedFormula = formula.replaceAll("[ ]+","")
            println(s"Parsing of $formula gives result:
 ${FormulaParser.parse(unspacedFormula)}")
        }
    }
}

If we execute this parser combinator code into Eclipse by simply right-clicking on the FormulaCalculator class and navigating to Run As | Scala Application, we should obtain the following output in the Eclipse console:

Parsing of 3*#ACCOUNT1# gives result:
 [1.13] parsed: BinaryOp(*,Number(3.0),Account(ACCOUNT1))
Parsing of #ACCOUNT1#- #ACCOUNT2# gives result:
 [1.22] parsed: BinaryOp(-,Account(ACCOUNT1),Account(ACCOUNT2))
Parsing of AVERAGE_UNDER_PERIOD(#ACCOUNT4#,+1,12) gives result:
 [1.39] parsed: AVERAGE_UNDER_PERIOD(Account(ACCOUNT4),+1,12)

This output shows that the three formulae were parsed correctly and converted into classes. The final evaluation is left out from this exercise but could be set up with some actual transactions defined on the two accounts.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.172.200