dsl design in scala
DESCRIPTION
Scala provides expressive syntax and many language features that make it easy to write natural DSLs. In this talk I will examine Scala features that are helpful for creating DSLs and talk about how you can use Scala to develop an intuitive DSL that is at the same level of abstraction as your problem domain.TRANSCRIPT
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!
})!
!