DSL DESIGN IN SCALA ZACK GRANNAN
WHAT IS A DSL
Language tailored for a specific Domain
Many Types and Sizes SQL, Matlab, Latex are big ones
EMBEDDED VS STAND-ALONE DSL
Stand-Alone: • Is a full language itself (e.g
HTML) Embedded:
• Built on top of existing language (e.g ScalaTest)
STAND ALONE Pros • Ultimate Freedom
Cons • Takes forever to develop. Don’t want to
reinvent the wheel • Difficult for others to learn and use (see
VimScript)
EMBEDDED
Pros • Take advantage of parent language • Much easier to develop
Cons • Forced to use syntax of parent
language • Often limited by parent language
DEEP AND SHALLOW EMBEDDED DSL • Shallow Embedded DSL
• DSL Expressions are converted immediately into non-DSL instructions in parent langauge
• Deep Embedded DSL
• DSL Expressions are converted into data structure (think AST). Structure can be executed, or modified.
• Maybe more work that shallow embedded DSL, but more powerful
WHY CREATE A DSL
Problem in Domain
Procedure in Domain
Solution in Domain
Procedure in Language
Solution in Language
A DSL is the Ultimate Abstraction – Paul Hudak
WHY CREATE A DSL
Problem in Domain
Procedure in Domain
Solution in Domain
A DSL is the Ultimate Abstraction – Paul Hudak
WHAT MAKES A GOOD DSL
• Syntax, Semantics match Domain • More Expressive in Domain • Less Powerful outside Domain Result: Cleaner Code, Fewer Bugs
WHEN SHOULD YOU MAKE A DSL?
Almost Always
WHY SCALA IS GOOD
• Syntactic Sugar • Infix, Postfix, Prefix, Symbolic
Operators • Implicit Conversion • Functional, Typesafe • Macros
SYNTACTIC SUGAR Semicolons are optional, as well as periods and parenthesis (in some cases). () and {} can be interchanged result.shouldBe(3) result shouldBe 3!
def unless(expr: Boolean)(perform: () => Any) {!
if (!expr) perform()!
}!
!
unless (2 == 1) {! () => println("Hello World")!
}// Outputs “Hello World”!
INFIX OPERATORS / SYMBOLIC OPERATORS Any method that takes one parameter is an infix operator a + b = a.+(b)! Symbolic operators can be used (and abused) trait Expr!
case class Lt(a: Expr, b: Expr) extends Expr!
case class IntExpr(int: Int) extends Expr {!
def < (other: IntExpr) = Lt(this, other)!
}!
!
IntExpr(1) < IntExpr(2) // Lt(IntExpr(1),IntExpr(2))!
POSTFIX AND PREFIX trait MyBool {!
def inverse : MyBool!
def unary_! : MyBool!
}!
!
case object True extends MyBool {!
def unary_! = False!
def inverse = False!
}!
!
case object False extends MyBool {!
def unary_! = True!
def inverse = True!
}!
!
println(!True) // Outputs False!
println(False inverse) // Outputs True
IMPLICIT CONVERSION case class Apples(amount: Int) {!
override def toString = s"There are $amount apples"!
}!
!
implicit class AppleInt(num: Int) {!
def apples = Apples(num)!
}!
!
println(10 apples) // “There are 10 apples”
IMPLICIT CONVERSION This is used to great effect in some libraries. scala.concurrent.duration val d = 5 millis!
val d2 = d * 2.5!
val d3 = d2 + 1.second!
!
Builtin:!
val range = 1 to 10
MACROS • Scala code that writes Scala code • Much better than C Macros • Blackbox Macro: Safe Macro
• Type checking can be done before macro invocation
• Whitebox Macro: Powerful Macro • Macro can introduce new types
UNLESS MACRO def unless(condition: Boolean)(thenExpr: Any): Unit = macro unlessImpl!
!
def unlessImpl(c: Context)(condition: c.Expr[Boolean])(thenExpr: c.Expr[Any]): c.universe.If = {!
import c.universe._!
q"if (!($condition)) {$thenExpr}”!
}!
!
---!!
unless (2 == 1) {!
println("Hello World")!
}!
!
if (!(2 == 1)) {! println("Hello World")!}
CUSTOM COMPILE-TIME ERRORS case class NonSpaceString(str: String)!
object NonSpaceString {!
implicit def fromString(s: String): NonSpaceString = macro makeNSString!
!
def makeNSString(c: Context)(s: c.Expr[String]) = {!
import c.universe._!
s match {!
case Expr(Literal(Constant(field))) =>!
val fieldString = showRaw(field)!
if (fieldString contains ' ') {!
throw new Exception(s"$fieldString contains a space character")!
}!
q"new NonSpaceString($fieldString)"!
}!
}!
}
CUSTOM COMPILE-TIME ERRORS import NonSpaceString._!
object Cl {!
def printNsString(s: NonSpaceString) {!
println(s.str)!
}!
!
def main(args: Array[String]) {!
printNsString("abc") // NonSpaceString!
!
printNsString("abc d") // Compile-time Error!
}!
}!
!
cl.scala:9: error: exception during macro expansion:!
java.lang.Exception: abc d contains a space character!
!at NonSpaceString$.makeNSString(mk.scala:16)!
!
printNsString("abc d")!
PUTTING IT ALL TOGETHER - RULE DSL Condition Action
Rule
CONDITION DEFINITION sealed trait Condition {!
def or (condition: Condition) = Or(this, condition)!
def and (condition: Condition) = And(this, condition)!
def unary_! = Not(this)!
}!
!
case class Or(c1: Condition, c2: Condition) extends Condition!
case class And(c1: Condition, c2: Condition) extends Condition!
case class Not(c: Condition) extends Condition!
case class DependentCondition(f: () => Boolean) extends Condition!
case object True extends Condition!
case object False extends Condition!
!
(True and !False) or (False or True)!
// Or(And(True,Not(False)),Or(False,True))!
!
CONDITION DEFINITION def eval(condition: Condition): Boolean = condition match {!
case And(c1, c2) => eval(c1) && eval(c2)!
case Or(c1, c2) => eval(c1) || eval(c2)!
case Not(c) => !eval(c)!
case DependentCondition(f) => f()!
case True => true!
case False => false!
}!
ACTION DEFINITION sealed trait Action {!
def andThen(action2: Action) = Then(this, action2)!
}!
!
case class Then(action1: Action, action2: Action) extends Action!
case class FunctionAction(function: () => Any) extends Action!
!
implicit def funcToAction(fun: () => Any) = {!
FunctionAction(fun)!
}!
!
def sayHello() { println("Hello") }!
def sayWorld() { println("World") }!
!
(sayHello _) andThen (sayWorld _) !
// Then(FunctionAction(<function0>),FunctionAction(<function0>))!
!
ACTION DEFINITION def eval(action: Action) {!
case Then(a1, a2) => eval(a1); eval(a2)!
case FunctionAction(f) => f()!
}!
RULE DEFINITION trait Rule {!
def condition: Condition!
def action: Action!
def elseAction: Option[Action] = None!
}!
!
case class IfRule(condition: Condition, action: Action) extends Rule {!
def otherwise(elseAction: Action) =!
IfElseRule(condition, action, elseAction)!
}!
!
case class IfElseRule(condition: Condition, action: Action, elseAct: Action) extends Rule {!
override def elseAction = Some(elseAct)!
}
RULE DEFINITION def eval(rule: Rule) {!
eval(rule.condition) match {!
case true => eval(rule.action)!
case false => rule.elseAction.foreach(eval)!
}!
}!
!
RULE DEFINITION def when(condition: Condition)(action: Action) = !
IfRule(condition, action)!
!
def unless(condition: Condition)(action: Action) = !
IfRule(Not(condition), action)!
!
when (True and False) {!
() => println("Won't see this")!
} otherwise {!
() => println("Will see this")!
}!
!
IfElseRule(!
And(True,False),!
FunctionAction(<function0>),!
FunctionAction(<function0>)!
)!
RULE DEFINITION eval {!
when (True and False) {!
() => println("Won't see this")!
} otherwise {!
() => println("Will see this")!
}!
}!
Will see this
QUESTIONS?
WHITEBOX MACRO object Macros {!
def makeInterface(s: String*): Any = macro makeInterfaceImpl!
def makeInterfaceImpl(c: Context)(s: c.Expr[String]*) = {!
import c.universe._!
val getters = s.map {!
case Expr(Literal(Constant(field))) =>!
val fieldName = showRaw(field)!
val tt = TermName(fieldName)!
val upper = fieldName.toUpperCase!
q"""def $tt = $upper"""!
}!
!
c.Expr(q"""!
case object Custom {!
..$getters!
};!
Custom""")!
}!
}!
!
val custom = !
makeInterface("abc", "def”)!
println(custom.abc) !
// Outputs "ABC”!
---------------------------------!
!
Expr[Nothing]({!
case object Custom extends scala.Product with scala.Serializable {!
def <init>() = {!
super.<init>();!
()!
};!
def abc = "ABC";!
def `def` = "DEF"!
};!
Custom!
})!
!