DSLs are everywhere. Have you ever used SQL, Ant or maybe HTML? If so you were using a DSL, maybe without realizing it. Domain-Specific Languages, or DSLs, provide convenient syntactical means of expressing goals in a given problem domain. A well-crafted DSL communicates the essence and means of the domain it represents in a natural way, so that you don’t even think about its underlying technology.
Scala’s rich, flexible syntax combined with its OO and functional features makes writing DSLs a breeze. In this talk I'll introduce the concept of DSLs, where to best apply them, their pros and cons, and how to integrate DSLs into your core application. We will see a practical example of how to lever the tools Scala gives us and build our very own tax calculation DSL.
5. A domain-specific language (DSL)
is a computer language specialized
to a particular application domain.“ “
6. DSLs are made to fit
their purpose only.
How is DSL different from any other
programming language?
● It is targeted at a specific problem domain
● It offers a higher level of abstraction to the user
10. Simple mapping is the challenge…
Problem
Domain
Solution
Implementation
11. Essential Complexity Accidental Complexity
● Sending a rocket to space
● Building a search engine
● Supporting high load of traffic
● Writing unclear code
● Over engineering
● Using the wrong tool for the job
15. Q&A
Flexible syntax, fit for abstraction
"123." contains(" 2)"<==>"123sniatnoc" ( "2" )
val a = 2<==>lav= a2 ;
"123." contains(" 2)"<==>"123sniatnoc"2" "
val name = "scala" >==< val name: String = "scala"
Optional dots in
method invocation
Semicolon
inference
Optional
parentheses
Type
inference
16. Q&A
We can easily pass complex logic
using simple expressions
Concise lambda
syntax
numbers map { x => x+1}
numbers sortWith { (x,y) => x>y }
numbers map {_+1} sortWith { _>_ }
17. Q&A
Case classes - perfect for abstraction design
case class Name (
val first: String = "hello",
val last: String = "world"
)
val myName = Name)first = "James ", last = "Bond")
18. Scala Implicits
● provide an implicit conversion from type to type
● statically typed
● allow us to create Lexically scoped open classes
19. Scala Implicits params example
object Main extends App {
def sayHello(name: String)(implicit greeting: String) {
println(s"$greeting, $name")
}
implicit val greeting = "Hi"
sayHello("George")
}
24. We are building a geo-based tax rule
matching system for shopping carts
25. In English, please
● “for Canada the tax is 15% of cart total"
● “for UK the tax is 12% of cart total, when calculating the tax ignore the shipping cost”
● “for USA the tax is 7% of cart total, when calculating the tax ignore the shipping cost and the discount”
● “for Finland the tax is 5% of cart total, when calculating the tax ignore discount if shipping costs larger than
4€"
● “for Israel the tax is 25% of cart total, add a special custom tourist tax”
We will want to translate this to a list of requirements
26. ● The tax for a shopping cart is calculated by the shipping country
● Tax is different for each country
● Tax calculation is depended on other factor like shipping and
discount
System requirements
27. ● Clear communication between project teams
● Tests speak the same language and can be reviewed by non
technical personal
Benefits of a common vocabulary
28. Break it down
● “for Canada the tax is 15% of cart total ”
● “for UK the tax is 12% of cart total, when calculating the tax ignore the shipping cost”
● “for USA the tax is 7% of cart total , when calculating the tax ignore the shipping cost and the
discount ”
● “for Finland the tax is 5% of cart total , when calculating the tax ignore discount if shipping costs
larger than 4 €”
● “for Israel the tax is 25% of cart total, add a special custom tourist tax”
29. ● Tax is calculated on a cart
● Cart is shipped to a specific country
● Cart might or might not have a discount ….
Understanding the relationship
30. Create domain abstractions
object Cart {
val shipping = (c: Cart) => c.shipping
val discount = (c: Cart) => c.discount
}
case class Cart( total: BigDecimal,
shipping: BigDecimal,
discount: BigDecimal,
country: Country)
31. Create domain abstractions
sealed trait Country
object Countries {
case object Israel extends Country
case object USA extends Country
case object FINLAND extends Country
case object UK extends Country
Case object CANADA extends Country
}
type CartPredicate = Cart => Boolean
type TaxCalculator = Cart => BigDecimal
type CountryToTaxCalculation = (Country, TaxCalculator)
32. “for Canada the tax is 15%”
def For(c: Country): CountryContainer = {
CountryContainer(c)
}
case class CountryContainer(country: Country) {
def take(tax: BigDecimal): CountryToTaxCalculation = {
(country, (cart: Cart) => cart.total * tax)
}
}
33. “for Canada the tax is 15%”
For(CANADA).take(0.15)
For(CANADA) take 0.15
34. whats going on under the hood ?
For(Canada) take 0.15
CountryContainer
(returns) (invokes)
(returns)
CountryToTaxCalculation
35. “for UK the tax is 12%, ignore the shipping costs”
Already supported
No!
Can we extend our solution
to support this part?
Next up
36. Add an intermediate step when needed
case class CountryContainer(country: Country) {
def take(tax: BigDecimal): CountryToTaxCalculation = {
(country, (cart: Cart) => cart.total * tax)
}
}
old
case class CountryContainer(country: Country) {
def take(tax: BigDecimal): TaxAndCountry =
TaxAndCountryContainer(country, tax)
}
new
37. Extend the vocabulary when needed
case class TaxAndCountryContainer
(country: Country, tax: BigDecimal) {
}
def ignore(cartField: CartField): CountryToTaxCalculation =
(country, (c: Cart) => (c.total - cartField(c)) * tax)
39. What's going on under the hood ?
For(UK) take 0.12 ignore shipping
CountryContainer
(returns) (invokes)
(returns)
TaxAndCountry
Container
(invokes)
(returns)
CountryToTaxCalculation
40. will “for Canada the tax is 15%” still work ??
For(CANADA) take 0.17
CountryContainer
(invokes)(returns)
(returns)
TaxAndCountryContainer != CountryToTaxCalculation
42. What's going on under the hood ?
For(CANADA) take 0.17
CountryContainer
(invokes)(returns)
(returns)
TaxAndCountry
(implicit conversion)
CountryToTaxCalculation
43. Workflow
Break the English
rules down
Understand the
relationships
Create domain
abstractions
Implement logic
structures
When needed:
• Add an intermediate step
• Extend the vocabulary
• Fix previous abstractions
44. Can we improve the ignore function
to take multiple params?
“for USA the tax is 12%, when calculating the tax ignore the shipping cost
and the discount ”
Next up
46. “for USA the tax is 12%, when calculating the tax ignore the shipping cost
and the discount ”
For(USA).take(0.12).ignore(discount, shipping)
We can do better...
47. taking it to the next level
object Cart {
val shipping = (c: Cart) => c.shipping
val discount = (c: Cart) => c.discount
}
object Cart {
val shipping = new CartCombinator((c: Cart) => c.shipping)
val discount = new CartCombinator((c: Cart) => c.discount)
}
48. CartCombinator
case class CartCombinator(val cartField: CartField) {
def and (comb: CartCombinator): CartCombinator =
new CartCombinator((c: Cart) => comb.cartField(c) + cartField(c))
}
49. TaxAndCountry – with combinators
case class TaxAndCountry(country: Country, tax: BigDecimal) {
def ignore(combs: CartCombinator*): CountryToTaxCalculation = {
def calculateTax(cart: Cart): BigDecimal =
(cart.total - combineAllRules(cart)) * tax
def combineAllRules(cart: Cart): BigDecimal =
combs.foldLeft(BigDecimal(0))((a, b) => a + b.cartField(cart))
(country, calculateTax)
}
50. “for USA the tax is 7%, when calculating the tax ignore the shipping cost
and the discount ”
For(USA).take(0.07).ignore(discount and shipping)
For(USA) take 0.07 ignore discount and shipping
51. Let’s add the & operator
class CartCombinator(val cartField: CartField) {
def and (comb: CartCombinator): CartCombinator =
new CartCombinator((c: Cart) => comb.cartField(c) + cartField(c))
def & (occ: CartCombinator): CartCombinator = and(occ)
}
52. “for USA the tax is 7%, when calculating the tax ignore the shipping cost
and the discount ”
For(USA).take(0.07).ignore(discount and shipping)
For(USA) take 0.07 ignore discount & shipping
53. operator precedence by order
(all letters)
|
^
&
< >
= !
:
+ -
* / %
(all other special characters)
54. What's going on under the hood ?
For(USA) take 0.07 ignore discount & shipping
CountryContainer
(invokes)(returns)
(returns)
TaxAndCountry
container
(invokes)
(returns)
CountryToTaxCalculation
(returns)
CartCombinator
55. Supported rules so far
For(CANADA) take 0.15
For(UK) take 0.12 ignore shipping
For(USA) take 0.07 ignore discount & shipping
“for Finland the tax is 5%, when calculating the tax ignore discount
if shipping costs larger than 4”
Next up
Unsupported
56. adding a predicate
class CartCombinator(val cartField: CartField) {
def If (cond: CartPredicate): CartCombinator =
new CartCombinator((c: Cart) =>
if (cond(c)) cartField(c) else 0)
def > (value: BigDecimal): CartPredicate =
((c: Cart) => cartField(c) > value)
. . .
57. adding a predicate
class CartCombinator(val cartField: CartField) {
def and (comb: CartCombinator): CartCombinator =
new CartCombinator((c: Cart) => comb.cartField(c) + cartField(c))
def & (occ: CartCombinator): CartCombinator = and(occ)
def If (cond: CartPredicate): CartCombinator =
new CartCombinator((c: Cart) =>
if (cond(c)) cartField(c) else 0)
def > (value: BigDecimal): CartPredicate =
((c: Cart) => cartField(c) > value)
}
58. “for Finland the tax is 5%, when calculating the tax ignore discount if
shipping costs larger than 4 ”
For(FINLAND).take(0.05).ignore (discount.If(shipping > 4))
For(FINLAND) take 0.05 ignore (discount If shipping > 4)
59. What's going on under the hood ?
For(FINLAND) take 0.05 ignore (discount If shipping > 4)
(invokes)
(returns)
CountryToTaxCalculation
CartCombinator
(returns)
CountryContainer
(invokes)(returns)
TaxAndCountry
container
(returns)
61. What's going on under the hood ?
For(Israel) take 0.25 addTouristPrice { cart => cart.total * 0.2}
CountryContainer
(invokes)(returns)
(returns)
TaxAndCountry
container
(invokes) (returns)
CountryToTaxCalculation
62. Tax Rules DSL
val taxRules: Seq[CountryToTax] = Seq(
For(CANADA) take 0.15 ,
For(UK) take 0.12 ignore shipping,
For(USA) take 0.07 ignore discount & shipping,
For(FINLAND) take 0.05 ignore (discount If shipping > 4),
For(Israel) take 0.25 addTouristPrice{ cart => cart.total * 0.2 }
)
63. English DSL
● “for Canada the tax is 15%" For(CANADA) take 0.15
● “for UK the tax is 12%, when calculating
the tax ignore the shipping cost”
For(UK) take 0.12 ignore shipping
● “for the USA the tax is 7%, ignore the
shipping cost and the discount"
For(USA) take 0.07
ignore discount & shipping
● “for Finland the tax is 5% ignore discount
if shipping costs larger than 4”
For(FIN) take 0.05
ignore (discount If shipping > 4)
● “for Israel the tax is 25%, add a special
custom tourist tax just for fun”
For(Israel) take 0.25
addTouristPrice {cart => cart.total * 0.2}
64. Matching tax rule for a cart
import TaxDsl._
val taxRules: Seq[CountryToTax]
val cart = Cart(total = 100, shipping = 10,discount =
20, country = USA)
val taxRule = taxRules findRuleFor cart
To activate this we need to
enrich an existing class
66. Errors and exceptions
● Always use the domain language
to express any exception that might occur during processing
● The compiler acts as the policeman for you
67. Final notes
● getting the syntax right is hard
● A DSL needs only to be expressive enough for the user
● DSLs are fun !
68. Q&A
This is where you are going to present your final words.
This slide is not meant to have a lot of text.Thank You!
Any Questions?
Alon Muchnick
@WixEngalonmu@wix.com
Editor's Notes
implicit are methods and fields that are wired implicitly by the scala compiler by searching for an matching signature in the current scope.
Scala offers the power of open classes through its implicit construct,
as we discussed in the Scala implicits sidebar in section 3.2.
Difference with Ruby monkey patching:
Scala implicits are lexically scoped; the added behavior via implicit
conversions needs to be explicitly imported into specific lexical scopes
(see [2] in section 6.10)