CHAPTER 10

image

DSL and Parser Combinator

The dichotomy of generic and specific manifests itself in the programming sphere. Domain-specific languages (DSLs) are one of the forms of the manifestations of this dichotomy. Domain-specific languages are just what they are called: domain specific. All programming languages are domain-specific languages when they come into existence, but that changes as they evolve. Domain-specific languages are created to solve problems in a certain area (or more precisely, in a certain domain) but as they gradually evolve to solve problems in several domains, the line that distinguishes them as specific blurs. Thus, such a language transgresses from the specific to the generic. A DSL is a special purpose language, the other extreme of which is a general purpose language, such as Scala and Java. Unfortunately, these general purpose languages have drawbacks, for example, if you want to execute a task on a database, then it is necessary to write a computer program for executing this task using a general purpose language. However, a DSL could be used to perform a number of such tasks on a database. And this is why some experts regard SQL, the structured query language, as a DSL.

In other words, a DSL is a special purpose programming language designed to express solutions to problems that belong to a particular problem domain. DSLs are focused on the domain or problem and can be of an external or internal type. An external DSL defines a new language with its own custom grammar and parser combinator. An internal DSL defines a new language as well, but within the syntactical boundaries of another language. No custom parser is necessary for internal DSLs. Instead, they are parsed just like any other code written in the language. Interest in DSLs has surged recently, driven in part by the Ruby 1 and Groovy 2 community, because they are very easy to implement in these languages. Ant3, which uses XML, is an example of an external DSL. Gant4, on the other hand, uses Groovy to solve the same problem and is an example of an internal DSL. Groovy with its metaprogramming5 capabilities and flexible syntax is better suited to designing and implementing internal DSLs. As an illustration, using Groovy’s optional parameters and MOP6, you can turn this into code that only a programmer can love:

println this.class.getResourceAsStream('readme.txt').getText()

into:

write 'readme.txt'.contents()

You do not have to be a Groovy programmer to notice that with the second option, even a ­non-programmer has a chance of understanding the intent of the code. As we’ll see, Scala provides excellent support for the creation of internal and external DSLs.

This chapter explains what a DSL is and how to write an internal DSL in Scala. This chapter then takes you through the building blocks of a parser called parser combinator in Scala, which is used to an write external DSL.

Domain Specific Language (DSL)

Domain specific language (DSL) is usually useful to simplify the interaction with a system by application to a small particular domain. DSL can be targeted to programmers by providing a simplified API to communicate with a system; or they may concern business users who might 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 two types of DSLs: internal and external DSLs.

Some well-known-examples of DSLs include ErlangOTP7, HTML, SQL, Verilog8, Mathematica9, YACC10. Xpath11, CSS12, YAML13, MATLAB14, and ANT.

Internal DSLs

Internal DSLs are most often embedded in a host language with the addition of syntactic sugar through tricks and special constructs of the language. Many of these languages support a meta-object protocol that you can use to implement dynamic behaviors onto your DSL. Most of these languages are dynamically typed, such as Ruby and Groovy. Groovy was used as a host language for DSL in the example at beginning of the chapter. Statically typed languages, such as Scala, offer abstraction capabilities to model your DSL.

Some of the features of Scala that make it a host language for an internal DSL are

  • Implicit conversions
  • Scala’s advanced type system
  • Currying
  • Infix and postfix operator notation of Scala
  • Syntactic sugar

For example, you can omit the parentheses and dot for any method that takes a single parameter as shown:

map.get("key") is equivalent to map get "key"

In this section you learn to build an internal DSL using Scala as a host language. Implicit conversion (see Chapter 8) gets you halfway to adding methods to a final class. The second half of the journey is that the Scala compiler looks to a possible implicit conversion from the type you have to a type with the method that you’re invoking. The Scala compiler inserts code to call the implicit conversion and then calls the method on the resulting instance. The ability to add new methods to existing classes has a lot of value for making code more readable and expressive. More importantly, implicit conversions make it possible to define DSLs in Scala. As a library producer, we can create syntactically pleasing ways to express concepts in a type-safe way. Wouldn’t it be nice to express a time span as 3 days or 15 seconds? That would make code a lot more readable than (3L * 24L * 3600L * 1000L). Wouldn’t it be great to set a timeout or a trigger with 2 hours later? Let’s define a library using implicit conversions (see Listing 10-1) and then break it down.

Listing 10-1. Timespan DSL

import java.util.Date
object TimeHelpers {
case class TimeSpanBuilder(val len: Long) {
def seconds = TimeSpan(TimeHelpers.seconds(len))
def second = seconds
def minutes = TimeSpan(TimeHelpers.minutes(len))
def minute = minutes
def hours = TimeSpan(TimeHelpers.hours(len))
def hour = hours
def days = TimeSpan(TimeHelpers.days(len))
def day = days
def weeks = TimeSpan(TimeHelpers.weeks(len))
def week = weeks
}
def seconds(in: Long): Long = in * 1000L
def minutes(in: Long): Long = seconds(in) * 60L
def hours(in: Long): Long = minutes(in) * 60L
def days(in: Long): Long = hours(in) * 24L
def weeks(in: Long): Long = days(in) * 7L
implicit def longToTimeSpanBuilder(in: Long): TimeSpanBuilder =
TimeSpanBuilder(in)
implicit def intToTimeSpanBuilder(in: Int): TimeSpanBuilder =
TimeSpanBuilder(in)
def millis = System.currentTimeMillis
case class TimeSpan(millis: Long) extends Ordered[TimeSpan] {
def later = new Date(millis + TimeHelpers.millis)
def ago = new Date(TimeHelpers.millis - millis)
def +(in: TimeSpan) = TimeSpan(this.millis + in.millis)
def -(in: TimeSpan) = TimeSpan(this.millis - in.millis)
def compare(other: TimeSpan) = millis compare other.millis
}
object TimeSpan {
implicit def tsToMillis(in: TimeSpan): Long = in.millis
}
class DateMath(d: Date) {
def +(ts: TimeSpan) = new Date(d.getTime + ts.millis)
def -(ts: TimeSpan) = new Date(d.getTime - ts.millis)
}
implicit def dateToDM(d: Date) = new DateMath(d)
}

We imported java.util.Date because we’re going to make use of it.

import java.util.Date
object TimeHelpers {

We then defined a class that takes a Long as a parameter and has a series of methods that convert the Long into a TimeSpanBuilder represented by the length.

case class TimeSpanBuilder(len: Long) {
def seconds = TimeSpan(TimeHelpers.seconds(len))
def second = seconds
def minutes = TimeSpan(TimeHelpers.minutes(len))
def minute = minutes
def hours = TimeSpan(TimeHelpers.hours(len))
def hour = hours
def days = TimeSpan(TimeHelpers.days(len))
def day = days
def weeks = TimeSpan(TimeHelpers.weeks(len))
def week = weeks
}

Then we defined a bunch of helper methods (called from TimeSpanBuilder) that convert to the correct number of milliseconds.

def seconds(in: Long): Long = in * 1000L
def minutes(in: Long): Long = seconds(in) * 60L
def hours(in: Long): Long = minutes(in) * 60L
def days(in: Long): Long = hours(in) * 24L
def weeks(in: Long): Long = days(in) * 7L

Next, we defined a bunch of implicit methods that convert from Int or Long into a TimeSpanBuilder. This allows the methods such as minutes or days on TimeSpanBuilder to appear to be part of Int and Long.

implicit def longToTimeSpanBuilder(in: Long): TimeSpanBuilder =TimeSpanBuilder(in)
implicit def intToTimeSpanBuilder(in: Int): TimeSpanBuilder = TimeSpanBuilder(in)

Then we defined a helper method that gets the current time in milliseconds:

def millis = System.currentTimeMillis

We defined the TimeSpan class that represents a span of time. We can do math with other Timespans or convert this TimeSpan into a Date by calling the later or ago methods. TimeSpan extends the Ordered trait so that we can compare and sort TimeSpans:

case class TimeSpan(millis: Long) extends Ordered[TimeSpan] {
def later = new Date(millis + TimeHelpers.millis)
def ago = new Date(TimeHelpers.millis - millis)
def +(in: TimeSpan) = TimeSpan(this.millis + in.millis)
def -(in: TimeSpan) = TimeSpan(this.millis - in.millis)

Next we compared this TimeSpan to another to satisfy the requirements of the Ordered trait:

def compare(other: TimeSpan) = millis compare other.millis
}

Then we defined a companion object that has an implicit method that will convert a Timespan into a Long. If there is an object with the same name as a class, that object is considered a companion object. If there are any implicit conversions defined in the companion object, they will be consulted if an instance of the class needs to be converted. We defined an implicit conversion from TimeSpan to Long in the companion object. This results in TimeSpan instances being automatically converted to Long if the TimeSpan is assigned to a Long variable or passed as a parameter that requires a Long.

object TimeSpan {
implicit def tsToMillis(in: TimeSpan): Long = in.millis
}

We can define TimeSpan instances with simple syntax, such as 3 days. Time Spans can be converted to Dates with the later and ago methods. But it would be helpful to add addition and subtraction of TimeSpans to Date instances. That’s pretty simple using implicit conversions. First, we defined a DateMath class that has + and - methods that take a TimeSpan as a parameter.

class DateMath(d: Date) {
def +(ts: TimeSpan) = new Date(d.getTime + ts.millis)
def -(ts: TimeSpan) = new Date(d.getTime - ts.millis)
}

Next we defined the implicit conversion:

implicit def dateToDM(d: Date) = new DateMath(d)

With all the 50 or so lines of code written, let’s see how it works.

scala> import TimeHelpers._
import TimeHelpers._
scala> 1.days
res0: TimeHelpers.TimeSpan = TimeSpan(86400000)
scala> 5.days + 2.hours
res1: TimeHelpers.TimeSpan = TimeSpan(439200000)
scala> (5.days + 2.hours).later
res2: java.util.Date = Mon Feb 16 19:11:29 PST 2009
scala> import java.util.Date
import java.util.Date
scala> val d = new Date("January 2, 2005")
d: java.util.Date = Sun Jan 02 00:00:00 PST 2005
scala> val lng: Long = 7.days + 2.hours + 4.minutes
lng: Long = 612240000

So, we’ve defined a nice DSL for time spans, and it converts itself to Long when necessary. We saw how Scala’s implicit conversions lead to very simple and concise DSLs. Choosing implicit conversions and designing domain-specific languages (DSLs) takes time, thought, and deliberation. Next, a brief introduction to external DSLs.

External DSLs

External DSLs build their own language-processing infrastructure: the parsers, the lexers, and the processing logic. You need to define a grammar (such as Backus–Naur Form15 (BNF)), that is, define all the rules that apply to parse a meaning or script successfully. The internal DSLs get this infrastructure free from the underlying host language, but you need to build them from scratch for external DSLs. In Scala, parser combinators are a notion close to the definition of BNF grammars and can provide very concise and elegant code when writing external DSLs. An external DSL has separate infrastructure for lexical analysis, parsing, interpretation, compilation, and code generation. When you write a parser for an external DSL, you can use a parser generator tool such as Antlr16. However, Scala includes a powerful parser combinator library that can be used for parsing most external DSLs that have a context-free grammar. Scala comes with a parser combinator library so you don’t have to implement your own language infrastructure. We discuss Scala parser combinator in the next section.

Parser Combinator

Parser combinators are one of the most important applications of functional programming. Parser combinators offer an internal DSL to use for designing external DSLs so you don’t have to implement your own language infrastructure as discussed earlier. Parser combinators are building blocks for parsers. Parsers that handle specific kinds of input can be combined to form other parser combinators for larger expressions. Scala comes with a parser combinator library that makes writing parsers simple. Furthermore, because your parser is written in Scala, there’s a single compilation step, and you get all the benefits of Scala’s type safety. In this section, we’re going to explore combinators and Scala’s parser combinatory library.

Higher-Order Functions and Combinators

Scala’s parser combinator library gives us a view of a powerful DSL, and it has its roots in a lot of computer science and mathematics. Let’s look at higher-order functions (functions that take functions as parameters), then at how higher-order functions can be combined to yield powerful functionality.

Higher-Order Functions

We’ve been using higher-order functions throughout this book. These are functions, or methods, which take functions as parameters. List.map is a higher-order function:

scala> List(1, 2, 3).map(_ + 1)
res0: List[Int] = List(2, 3, 4)

We’ve also seen how to compose functions:

scala> def plus1(in: Int) = in + 1
plus1: (Int)Int
scala> def twice(in: Int) = in * 2
twice: (Int)Int
scala> val addDouble = plus1 _ andThen twice
addDouble: (Int) => Int = <function>
scala> List(1,2,3).map(addDouble)
res2: List[Int] = List(4, 6, 8)

In this example, we’ve composed a function, addDouble, out of two other functions, plus1 and twice. We can compose complex functions. We can even compose functions dynamically based on user input. We saw an example of this in the “Building New Functions” section in Chapter 4.

Combinators

You might be asking, what is a parser combinator? A combinator is a function that takes only other functions as parameters and returns only functions. Combinators allow you to combine small functions into big functions. In the case of the parser combinator library, you can combine small functions that match individual characters or small groups of characters into bigger functions that can parse complex documents. So, you have input a Seq[Char] (sequence of characters), and you want to parse the stream, which will either contain t, r, u, e or f, a, l, s, e—true or false. So, you would express such a program as

def parse = (elem('t') ~ elem('r') ~ elem('u') ~ elem('e')) |
(elem('f') ~ elem('a') ~ elem('l') ~ elem('s') ~ elem('e'))

where the elem method returns a subclass of Function[Seq[Char], ParseResult[Char]] that also has ~ and | methods. The first call to elem returns a function that will attempt to match the first character in an input stream to the letter “t.” If the first letter of the input stream matches, then the function returns Parsers.Success; otherwise it returns a Parsers.NoSuccess. The ~ method is called “and then,” so we can read the first part as t, then r , then u, and then e. So, elem('t') ~ elem('r') returns another one of these special Function[Seq[Char], ParseResult[List[Char]]] things. So we combine the functions with the ~ method into one bigger function. We keep doing this with each successive ~ method invocation. The following code:

elem('t') ~ elem('r') ~ elem('u') ~ elem('e')

builds a single function that consults the characters t, r, u, e and returns a Parsers.Success[List[Char]] or a Parsers.NoSuccess if the input does not contain t, r, u, e. The | operator also takes two of these combinated function thingies and combines them into a single function thingy that tests the first clause, true, and if that succeeds, its value is returned, but if it does not succeed, then the second clause, false, is tried. Let’s call that function thingy a Parser. So, we can combine these Parser instances with each other into other Parser instances using operators such as “and then,” “or else,” and so on. We can combine little Parsers into big Parsers using logic and thus construct complex grammars out of little building blocks. Let’s use a little bit of Scala’s implicit functionality to make the definition of our grammar easier. Scala’s parser combinator library has implicit conversions from Char into Parser[Char], so we can write

def p2 = ('t' ~ 'r' ~ 'u' ~ 'e') |
('f' ~ 'a' ~ 'l' ~ 's' ~ 'e')

Yes, that definitely looks better. But, there’s still a question of what these Parsers return when we pass a Seq[Char] into them. Or put another way, we want to get a Boolean true or false when we pass our input into them. So, let’s define the return type of our expression:

def p3: Parser[Boolean] = ('t' ~ 'r' ~ 'u' ~ 'e') |
('f' ~ 'a' ~ 'l' ~ 's' ~ 'e')

That’s what we want, but the compiler complains that it doesn’t know how to convert the combined Parser into a Boolean. So, let’s add a little bit of code to tell the Parser how to convert its result into a Boolean.

def p3: Parser[Boolean] = ('t' ~ 'r' ~ 'u' ~ 'e' ^^^ true) |
('f' ~ 'a' ~ 'l' ~ 's' ~ 'e' ^^^ false)

That works. The ^^^ method on Parser says, “If we match the input, return this constant.” We’ve built a function that will match true or false and return the appropriate Boolean value if either pattern of characters is matched. But we can also use the characters that are part of the pattern to create the value returned when the input is applied to the function using the ^^ method. We’ll define positiveDigit and digit Parsers:2

def positiveDigit = elem('1') | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
def digit = positiveDigit | '0'

In positiveDigit, we needed to specify elem('1') as the first part of the expression because '1' | '2' is a legal expression, so the implicit conversion of '1' to elem('1')does not take place. Note that we combined the positiveDigit Parser with elem('0') into a Parser that accepts all digits. Let’s make this into a Parser that converts the digits into a Long:

def long1: Parser[Long] = positiveDigit ~ rep(digit) ^^ {
case (first: Char) ~ (rest: List[Char]) => (first :: rest).mkString.toLong
}

We create a Parser that matches a positiveDigit and then zero or more digits using rep(digit). If application of the predicate (positiveDigit ~ rep(digit)) succeeds, then we convert to a Long by applying the conversion function:

case (first: Char) ~ (rest:List[Char]) => (first :: rest).mkString.toLong.

The ^^ method on Parser causes the conversion function to be applied if the predicate succeeds. In this example, I was explicit about the types, but the type inferencer will get it right. Let’s tighten up the example a little by only accepting rest if it’s fewer than 18 digits so we don’t overflow the Long:

lazy val long2: Parser[Long] = positiveDigit ~ rep(digit) ^? {
case first ~ rest if rest.length < 18 => (first :: rest).mkString.toLong
}

In this case, we’ve used the ^? method to connect the predicate to the conversion. In order for the Parser to succeed, we need to satisfy the predicate, and the partial function passed to ^? must be defined for the result of the predicate. In this case, the partial function will be satisfied if the length of rest is fewer than 18 characters. We’ve also changed from a method to a lazy val. This is because the method does not do the parsing; rather, the method combines smaller Parsers into a single Parser. This building of the Parser need only happen once, and the resulting Parser can be used over and over, even simultaneously on multiple threads. With the basics under our belt, let’s put our parser mojo to use.

The Calculator Parser

In this section, we’re going to use the parser combinator to build a four-function calculator. Yes, it’s time to swat flies with a Buick. You’ll see how easy it is to describe what we want to build, create a Parser for it, and then make sure the Parser returns the correct things. But first, let’s define a utility trait that will allow us to more easily run the Parsers from the Scala REPL. The RunParser trait can be mixed into any Parser and adds a run method.

import scala.util.parsing.combinator._
trait RunParser {
this: RegexParsers =>
type RootType
def root: Parser[RootType]
def run(in: String): ParseResult[RootType] = parseAll(root, in)
}

The RunParser trait can be mixed into a class that extends RegexParsers. By mixing RunParser into your Parser, you can type MyParser.run("Thing to test") and see the result. It’s a convenience trait. We'll define the skeleton of our four-function calculator. Let’s first describe how our calculator works. A sum expression is a product expression followed by zero or more + or –symbols followed by a product expression. A product expression is a factor followed by zero or more * or / symbols followed by another factor. This means that the precedence of production expressions is higher than the precedence of sum expressions. Finally, we define a factor as a number or parentheses around a sum expression. In BNF, we’d write

<sumExpr> ::= <prodExpr> [("+" <prodExpr>) | ("-" <prodExpr>)]
<prodExpr> ::= <factor> [("*" <factor>) | ("/" <factor>)]
<factor> ::= <float> | ("(" <sumExpr> ")")

We’ve described our parsing rules in English and BNF. Now let’s see how that translates to Scala.

object CalcSkel extends JavaTokenParsers with RunParser {
lazy val sumExpr = multExpr ~ rep("+" ~ multExpr | "-" ~ multExpr)
lazy val multExpr = factor ~ rep("*" ~ factor | "/" ~ factor)
lazy val factor: Parser[Any] = floatingPointNumber | "(" ~ sumExpr ~ ")"
type RootType = Any
def root = sumExpr
}

We’ve extended JavaTokenParsers, which gives us access to a bunch of stuff that will parse tokens as defined by the Java Language Specification. We’re taking advantage of floatingPointNumber and automatic white space consumption between elements. Cool. Let’s see how this works in the REPL.

scala> CalcSkel.run("1")
res0: [1.2] parsed: ((1~List())~List())
scala> CalcSkel.run("1 + 1")
res1: [1.6] parsed: ((1~List())~List((+~(1~List()))))
scala> CalcSkel.run("1 + 1 / 17")
res2: [1.11] parsed: ((1~List())~List((+~(1~List((/~17))))))
scala> CalcSkel.run("1 + 1 / archer")
res3: CalcSkel.ParseResult[CalcSkel.RootType] =
[1.9] failure: `(' expected but ` ' found
1 + 1 / archer
^

This is pretty encouraging. Our English and BNF descriptions of what we wanted to parse correspond very closely to our Scala code. Furthermore, our parse correctly parses valid input and rejects input with errors in it. The results, however, are pretty tough to read. Next, let’s turn the results into something that performs the calculations. Our Parser doesn’t change, but we add a function to convert the parsed items into a Double. First comes Listing 10-2 and then we’ll comb through the code.

Listing 10-2. Calculator parser

import scala.util.parsing.combinator._
object Calc extends JavaTokenParsers with RunParser {
lazy val sumExpr = prodExpr ~
rep("+" ~> prodExpr ^^ (d => (x: Double) => x + d) |
"-" ~> prodExpr ^^ (d => (x: Double) => x - d)) ^^ {
case seed ~ fs => fs.foldLeft(seed)((a, f) => f(a))
}
lazy val prodExpr = factor ~
rep("*" ~> factor ^^ (d => (x: Double) => x * d) |
"/" ~> factor ^^ (d => (x: Double) => x / d)) ^^ {
case seed ~ fs => fs.foldLeft(seed)((a, f) => f(a))
}
lazy val factor: Parser[Double] =
floatingPointNumber ^^ (_.toDouble) | "(" ~> sumExpr <~ ")"
type RootType = Double
def root = sumExpr
}

First we import the appropriate classes and then get down to business:

import scala.util.parsing.combinator._
object Calc extends JavaTokenParsers with RunParser {
lazy val sumExpr = prodExpr ~
rep("+" ~> prodExpr ^^ (d => (x: Double) => x + d) |
"-" ~> prodExpr ^^ (d => (x: Double) => x - d)) ^^ {
case seed ~ fs => fs.foldLeft(seed)((a, f) => f(a))
}

The rep method results in a list of whatever is parsed by the parameter of rep. When we match the + ~>prodExpr, we convert this into a function that adds the two numbers. Please note the ~> method. This method matches both items but only passes the stuff on the right to the converter function. There’s a corresponding <~ operator. Back in the code we’ve got a prodExpr, which is a Parser[Double] and then a Parser[List[Double => Double]], and we need to convert this into a Parser[Double]. The line

case seed ~ fs => fs.foldLeft(seed)((a, f) => f(a))

extracts the seed and the list of functions (add or subtract) and uses foldLeft to perform the calculation. We do the same for multiplication and division:

lazy val prodExpr = factor ~
rep("*" ~> factor ^^ (d => (x: Double) => x * d) |
"/" ~> factor ^^ (d => (x: Double) => x / d)) ^^ {
case seed ~ fs => fs.foldLeft(seed)((a, f) => f(a))
}

Next, we define factor, which is either a number or parentheses around a sumExpr. Because sumExpr, prodExpr, and factor reference each other and, thus, are recursive, we must define the type of at least one of the three vals so the type inferencer can do its work.

lazy val factor: Parser[Double] =
floatingPointNumber ^^ (_.toDouble) | "(" ~> sumExpr <~ ")"

We convert floating PointNumber into a Double by passing a function that converts a String to a Double. Next, we use ~> and <~ to discard the parentheses around sumExpr.Calc mixes in RunParser, so we have to define the abstract RootType type and the root method.

type RootType = Double
def root = sumExpr
}

That’s it. We’ve defined the conversions from the Strings and List to Doubles. Let’s see how well it works.

scala> Calc.run("1")
res0: Calc.ParseResult[Calc.RootType] = [1.2] parsed: 1.0
scala> Calc.run("1 + 1")
res1: Calc.ParseResult[Calc.RootType] = [1.6] parsed: 2.0
scala> Calc.run("1 + 1 / 17")
res2: Calc.ParseResult[Calc.RootType] = [1.11] parsed: 1.0588235294117647
scala> Calc.run("(1 + 1) / 17")
res3: Calc.ParseResult[Calc.RootType] = [1.13] parsed: 0.11764705882352941

In this section, we’ve converted from a BNF description of the grammar to a running application using Scala’s parser combinator library. The simple example was a four-function calculator.

This brief introduction concludes our tour of DSLs and parser combinators. DSL is a broad and rapidly emerging arena and deserves a book of its own. With a good sense of the business domain and the coding conventions, you may design domain-specific languages for use by other team members. DSLs deliver value because they allow the program to more closely match the language that business people use to describe solutions in a given domain. As we’ve seen with Scala’s parser combinator library as well as Specs, Scala makes it easy to create code that corresponds to the language a human would use to describe the answer to a problem. If your business people understand the code, and they should if the DSLs are well crafted, they will be able to give direct feedback as to the program reflecting the business rules.

Summary

Scala’s parser combinator library demonstrates the flexibility of Scala’s syntax, the usefulness of implicit conversions, and the power of functional composition. The parser combinator is an excellent example of a domain-specific language. The domain is parsing text, and the syntax is nearly one-for-one with BNF. This library also gives you some idea of the kind of domain-specific languages you can create using Scala. There’s nothing specific in the Scala compiler for the parser combinator library—it’s just that, a library. On a practical level, using a single language—Scala—for defining your Parser rather than using a tool like ANTLR means that you and your team use a single language for describing your system. This means that your brain thinks Scala. This means that you edit code in a single language and take advantage of the type safety of the language. In the next chapter, we’ll explore how you can integrate Scala into your projects and take advantage of the power of Scala without losing the infrastructure that you’ve built around Java.

_________________________

1https://www.ruby-lang.org/en/

2http://groovy.codehaus.org/

3http://ant.apache.org/

4http://gant.codehaus.org/

5http://en.wikipedia.org/wiki/Metaprogramming

6http://c2.com/cgi/wiki?MetaObjectProtocol

7http://www.erlang.org/faq/introduction.html

8http://www.verilog.com/

9http://www.wolfram.com/mathematica/

10http://dinosaur.compilertools.net/yacc/

11http://www.w3.org/TR/xpath/

12http://www.w3.org/Style/CSS/Overview.en.html

13http://en.wikipedia.org/wiki/YAML

14http://nl.mathworks.com/products/matlab/

15http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form

16http://www.antlr.org/

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

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