building dsls with scala

68
Building DSLs With Scala github.com/alonmuch @WixEng [email protected] m Alon Muchnick

Upload: alon-muchnick

Post on 13-Jan-2017

49 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Building DSLs with Scala

Building DSLs With Scala

github.com/alonmuch@[email protected]

Alon Muchnick

Page 2: Building DSLs with Scala

When you order coffee, what do you ask for?

Page 3: Building DSLs with Scala

Once a language’s corner stones are established, its common vocabulary can be expanded.

Page 4: Building DSLs with Scala

Introduction to DSLs

01

Page 5: Building DSLs with Scala

A domain-specific language (DSL) is a computer language specialized to a particular application domain.

“ “

Page 6: Building DSLs with Scala

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

Page 7: Building DSLs with Scala

Coffee DSL

Page 8: Building DSLs with Scala

SQL

Page 9: Building DSLs with Scala

CSS

Page 10: Building DSLs with Scala

Simple mapping is the challenge…

Problem Domain

Solution Implementation

Page 11: Building DSLs with Scala

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

Page 12: Building DSLs with Scala

DSLs reduces the accidental complexity

Page 13: Building DSLs with Scala

DSLs reduces the accidental complexity

Page 14: Building DSLs with Scala

Why Scala ?

02

Page 15: Building DSLs with Scala

Q&AFlexible syntax, fit for abstraction

"123".contains("2") <==> "123"contains("2")

val a = 2 <==> val a = 2;

"123".contains("2") <==> "123"contains2""

val name = "scala" <==> val name: String = "scala"

Optional dots in method invocation

Semicolon inference

Optional parentheses

Type inference

Page 16: Building DSLs with Scala

Q&AWe 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 { _>_ }

Page 17: Building DSLs with Scala

Q&ACase classes - perfect for abstraction design

case class Name ( val first: String = "hello", val last: String =" world ")

val myName = Name(first = "James", last =" Bond")

Page 18: Building DSLs with Scala

Scala Implicits ●provide an implicit conversion from type to type

●statically typed

●allow us to create Lexically scoped open classes

Page 19: Building DSLs with Scala

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")}

Page 20: Building DSLs with Scala

Scala Implicits conversiondef intOnly(int: Int) = { . . .}

intOnly(1.1)

“Type mismatch, expected Int actual Double”

Page 21: Building DSLs with Scala

Scala Implicits conversion

def intOnly(int: Int) = { . . .}

intOnly(1.1)

implicit def double2int(x: Double): Int = x.toInt

Page 22: Building DSLs with Scala

Scala open classes

Map(1 -> "one", 2 -> "two", 3 -> "three")

implicit final class ArrowAssoc[A](self : A) { @scala.inline def ->[B](y : B) : scala.Tuple2[A, B] = { /* compiled code */ } def →[B](y : B) : scala.Tuple2[A, B] = { /* compiled code */ }}

Page 23: Building DSLs with Scala

Lets build a DSL !

03

Page 24: Building DSLs with Scala

We are building a geo-based tax rule matching system for shopping carts

15%

12%12%

25%

Page 25: Building DSLs with Scala

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

Page 26: Building DSLs with Scala

● 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

Page 27: Building DSLs with Scala

● Clear communication between project teams

● Tests speak the same language and can be reviewed

by non technical personal

Benefits of a common vocabulary

Page 28: Building DSLs with Scala

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”

Page 29: Building DSLs with Scala

●Tax is calculated on a cart

●Cart is shipped to a specific country

●Cart might or might not have a discount ….

Understanding the relationship

Page 30: Building DSLs with Scala

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)

Page 31: Building DSLs with Scala

Create domain abstractionssealed trait Countryobject 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)

Page 32: Building DSLs with Scala

“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) }}

Page 33: Building DSLs with Scala

“for Canada the tax is 15%”

For(CANADA).take(0.15)

For(CANADA) take 0.15

Page 34: Building DSLs with Scala

whats going on under the hood ?For(Canada) take 0.15

CountryContainer

(returns) (invokes)(returns)

CountryToTaxCalculation

Page 35: Building DSLs with Scala

“for UK the tax is 12%, ignore the shipping costs”

Already supported

No!

Can we extend our solution to support this part?

Next up

Page 36: Building DSLs with Scala

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

Page 37: Building DSLs with Scala

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)

Page 38: Building DSLs with Scala

For(UK).take(0.12).ignore(shipping)

For(UK) take 0.12 ignore shipping

“for UK the tax is 12%, ignore the shipping costs”

Page 39: Building DSLs with Scala

What's going on under the hood ?For(UK) take 0.12 ignore shipping

CountryContainer

(returns) (invokes)(returns)

TaxAndCountry Container

(invokes)(returns)

CountryToTaxCalculation

Page 40: Building DSLs with Scala

will “for Canada the tax is 15%” still work ??

For(CANADA) take 0.17

CountryContainer

(invokes)(returns)(returns)

TaxAndCountryContainer!= CountryToTaxCalculation

Page 41: Building DSLs with Scala

Fix when needed

implicit def taxAndCountryToTaxCalculation (taxAndCountry: TaxAndCountry): CountryToTaxCalculation = { (taxAndCountry.country, { (cart: Cart) => (cart.total * taxAndCountry.tax) })}

Page 42: Building DSLs with Scala

What's going on under the hood ?For(CANADA) take 0.17

CountryContainer

(invokes)(returns)(returns)

TaxAndCountry

(implicit conversion)

CountryToTaxCalculation

Page 43: Building DSLs with Scala

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

Page 44: Building DSLs with Scala

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

Page 45: Building DSLs with Scala

TaxAndCountry

def ignore(cartField: CartField): CountryToTaxCalculation = (country, (c: Cart) => (c.total - cartField(c)) * tax)

old

def ignore(cartFields: CartField*): CountryToTaxCalculation = (country, (c: Cart) =>

(c.total - cartFields.foldLeft(BigDecimal(0))((a, f) => a + f(c))) * tax)

new

Page 46: Building DSLs with Scala

“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...

Page 47: Building DSLs with Scala

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)}

Page 48: Building DSLs with Scala

CartCombinator

case class CartCombinator(val cartField: CartField) {

def and (comb: CartCombinator): CartCombinator = new CartCombinator((c: Cart) => comb.cartField(c) + cartField(c))

}

Page 49: Building DSLs with Scala

TaxAndCountry – with combinatorscase 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)

}

Page 50: Building DSLs with Scala

“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

Page 51: Building DSLs with Scala

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)

}

Page 52: Building DSLs with Scala

“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

Page 53: Building DSLs with Scala

operator precedence by order(all letters)|^&< >= !:+ -* / %(all other special characters)

Page 54: Building DSLs with Scala

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

Page 55: Building DSLs with Scala

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

Page 56: Building DSLs with Scala

adding a predicateclass 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)

. . .

Page 57: Building DSLs with Scala

adding a predicateclass 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)}

Page 58: Building DSLs with Scala

“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)

Page 59: Building DSLs with Scala

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)

Page 60: Building DSLs with Scala

Adding custom values

case class TaxAndCountry(country: Country, tax: BigDecimal) {

def addTouristPrice (f: Cart => BigDecimal): CountryToTaxCalculation = addCustomValue(f)

private def addCustomValue (f:CartField): CountryToTaxCalculation = (country, (cart: Cart) => (cart.total + f(cart)) * tax)

def ignore(fs: CartCombinator*): CountryToTaxCalculation = {

. . .

}

Page 61: Building DSLs with Scala

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

Page 62: Building DSLs with Scala

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 }

)

Page 63: Building DSLs with Scala

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}

Page 64: Building DSLs with Scala

Matching tax rule for a cartimport 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

Page 65: Building DSLs with Scala

Enriching Seq implicit class TaxRuleSeq(taxRules: Seq[CountryToTaxCalculation]) { def findRuleFor(cart: Cart) = { taxRules.find(_._1 == cart.country).map(_._2) }}

Page 66: Building DSLs with Scala

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

Page 67: Building DSLs with Scala

Final notes

●getting the syntax right is hard●A DSL needs only to be expressive enough for the user●DSLs are fun !

Page 68: Building DSLs with Scala

Q&AThis 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

@[email protected]