O slideshow foi denunciado.
Seu SlideShare está sendo baixado. ×

A Field Guide to DSL Design in Scala

Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Carregando em…3
×

Confira estes a seguir

1 de 54 Anúncio

A Field Guide to DSL Design in Scala

Baixar para ler offline

A talk given at ScalaUA 2016 in Kiev, Ukraine.

Scala combines a powerful type system with a lean but flexible syntax. This combination enables incredible flexibility in library design, most particularly in designing internal DSLs for many common scenarios: specification definition and matching in Specs² and ScalaTest, request routing in Spray and query construction in Squeryl, just to name a few. The budding DSL designer, however, will quickly realize that there are precious few resources on how to best approach the problem in Scala; the various techniques, limitations and workarounds are not generally well understood or documented, and every developer ends up running into the same challenges and dead-ends. In this talk I'll attempt to summarize what I've learned from reading, extending and designing Scala DSLs in the hopes that it'll save future Scala library designers a whole lot of pain.

A talk given at ScalaUA 2016 in Kiev, Ukraine.

Scala combines a powerful type system with a lean but flexible syntax. This combination enables incredible flexibility in library design, most particularly in designing internal DSLs for many common scenarios: specification definition and matching in Specs² and ScalaTest, request routing in Spray and query construction in Squeryl, just to name a few. The budding DSL designer, however, will quickly realize that there are precious few resources on how to best approach the problem in Scala; the various techniques, limitations and workarounds are not generally well understood or documented, and every developer ends up running into the same challenges and dead-ends. In this talk I'll attempt to summarize what I've learned from reading, extending and designing Scala DSLs in the hopes that it'll save future Scala library designers a whole lot of pain.

Anúncio
Anúncio

Mais Conteúdo rRelacionado

Diapositivos para si (20)

Anúncio

Semelhante a A Field Guide to DSL Design in Scala (20)

Mais de Tomer Gabel (20)

Anúncio

Mais recentes (20)

A Field Guide to DSL Design in Scala

  1. 1. A Field Guide to DSL Design Tomer Gabel @ ScalaUA 2016
  2. 2. SETTING THE STAGE
  3. 3. DSLs • Target specific application domains • Designers can make more assumptions • This enables optimized syntax for: –Conciseness –Correctness –Readability
  4. 4. External DSL • Standalone grammar • Explicit library/tool support (lexer, parser) • Explicit lifecycle
  5. 5. Examples • Data querying with SQL: SELECT s1.article, s1.dealer, s1.price FROM shop s1 LEFT JOIN shop s2 ON s1.article = s2.article AND s1.price < s2.price WHERE s2.article IS NULL;
  6. 6. Examples • Text formatting with Markdown: Download -------- [Markdown 1.0.1][dl] (18 KB) -- 17 Dec 2004 [dl]: http://daringfireball.net/projects/downloa ds/Markdown_1.0.1.zip
  7. 7. Internal DSL • Extends some host language • Exploits the host to add new syntax –Within the confines of the host –Cannot add new syntactic constructs –Usage is valid code in the host language • Lifecycle is managed by the host
  8. 8. Examples • Testing with ScalaTest: "Empty combinator" should { "successfully validate an empty sequence" in { val sample = Seq.empty[String] val validator = new Empty[Seq[String]] validator(sample) should be(aSuccess) } "render a correct rule violation" in { val sample = Some("content") val validator = new Empty[Option[String]] validator(sample) should failWith("must be empty") } }
  9. 9. Examples • Request routing with Scalatra: get("/guess/:who") { params("who") match { case "Frank" => "You got me!" case _ => pass() } }
  10. 10. Examples • JSON AST construction with Play: Json.obj("users" -> Json.arr( Json.obj("name" -> "bob", "age" -> 31, "email" -> "bob@gmail.com"), Json.obj("name" -> "kiki", "age" -> 25, "email" -> JsNull) ))
  11. 11. Shallow Embedding • Eager evaluation • An expression results in effects • I prefer the term “imperative DSL” • Examples: – Ant tasks – Assertions
  12. 12. Deep Embedding • Pure and lazy • An expression results in an execution plan – Prescribes behavior to subsequent logic – I prefer the term “prescriptive DSL” • Examples: – Request routing – Query languages
  13. 13. Summary • Deep embedding is more powerful – Evaluation can be deferred, optimized and repeated • But more complex to implement: – Result domain model – Requires separate execution logic • Example shown with shallow embedding
  14. 14. STEP 1: KNOW YOUR DOMAIN
  15. 15. Specificity • Identify your actors • What actions can they take? • What objects do they act on? • What are the consequences of these actions?
  16. 16. Example: Assertions • Caller has a piece of data • And an assumption on its shape • Caller asserts that the assumption holds • If not, an exception is thrown Actor Object Object Action Consequenc e
  17. 17. Audience • Know your users • Which actor do they represent? • What consequences do they desire? • How would they express this desire?
  18. 18. Example: Assertions • Only one actor and consequence • But how to best express this desire? • Start with the user’s perspective: –We already have some data –We need a way to define assumptions –And concise syntax for assertions
  19. 19. Articulation • Choose a vocabulary: – Nouns describe objects – Adjectives qualify objects – Verbs describe actions – Adverbs qualify actions
  20. 20. Example: Assertions • An assertion is a sentence: “list should be empty” Object (noun) Action (verb) Qualifier (adjective) Actor is implied
  21. 21. Example: Assertions • Assumptions (simple) – empty – null – true – false – left – right – defined – completed – … • Assumptions (parameterized) – equalTo – startWith – endWith – contain – matchRegex • Modifiers (adverbs) – not
  22. 22. STEP 2: KNOW YOUR LANGUAGE
  23. 23. Infix Notation • Also known as “dot free syntax” • Basic building block in fluent DSLs • Applies to arity-1 function applications object.method(parameter) object method parameter
  24. 24. Infix Notation: Caveats • Chaining must be done with care list should be empty • This is what we expect. Object Method Parameter
  25. 25. Infix Notation: Caveats • Chaining must be done with care list should be empty • Infix notation is bloody literal! • What is the type of be? – Probably not what you meant Object Method Parameter
  26. 26. Infix Notation: Caveats • Workaround 1: Contraction  list shouldBe empty • Workaround 2: Parentheses  list should be(empty) • There is no “right” answer – It’s an aesthetic preference
  27. 27. Infix Notation: Caveats • Chaining must be done with care list should be empty Object Method Parameter
  28. 28. Infix Notation: Caveats • Chaining must be done with care list should be empty • Must have odd number of participants • This expression is illegal in Scala! – (Unless you use postfix operators. Don’t.) Object Method Parameter ?
  29. 29. Implicit Classes • Provide an entry point into your DSL • The extended type is domain-specific list shouldBe empty • Also used to lift values into your domain – More on this later Known a-priori Extension Method
  30. 30. STEP 3: PUTTING IT TOGETHER
  31. 31. Foundation • An assertion is a sentence with the shape: data shouldBe predicate • We first need a predicate definition: trait Predicate[-T] { def test(data: T): Boolean def failure: String }
  32. 32. Foundation • Next, we need an entry point into our DSL • Data is the only known entity • We’ll need to extend it: implicit class ValidationContext[T](data: T) { def shouldBe(predicate: Predicate[T]) = ??? }
  33. 33. Foundation • This is a shallowly-embedded DSL • Assertion failure has consequences • In our case, an exception is thrown: implicit class ValidationContext[T](data: T) { def shouldBe(predicate: Predicate[T]) = require(predicate.test(data), s"Value $data ${predicate.failure}") }
  34. 34. Foundation • We can start implementing predicates: someList shouldBe empty • A generic solution is fairly obvious: def empty[T <: Iterable[_]] = new Predicate[T] { def test(data: T) = data.isEmpty def failure = "is not empty" }
  35. 35. NOW THINGS GET HAIRY So far, so good.
  36. 36. Keywords • What about booleans? (3*4 > 10) shouldBe true • Booleans are reserved keywords – Can’t provide a def – Can’t provide an object • Can we support this syntax?
  37. 37. Keywords • Workaround: Lift via an implicit class implicit class BooleanPredicate(b: Boolean) extends Predicate[Boolean] { def test(data: Boolean) = data == b def failure = s"is not $b" }
  38. 38. Keywords • There’s a similar issue with null: val ref: String = null ref shouldBe null • But lifting won’t work: def shouldBe(predicate: Predicate[T]): Unit • Null is bottom type, extends Predicate[T] • Implicit search does not take place!
  39. 39. Keywords • Workaround: Specific method overload – Should only apply when T is a reference type implicit class ValidationContext[T](data: T) { // ... def shouldBe(n: Null) (implicit ev: T <:< AnyRef): Unit = require(data == null, s"Value $data is not null") }
  40. 40. Parameterization • What about parameterized predicates? 3*4 shouldBe equalTo 12 • The equality predicate is simple enough: def equalTo[T](rhs: T) = new Predicate[T] { def test(data: T) = data == rhs def failure = s"is not equal to $rhs" } • But we have an even number of parts!
  41. 41. Parameterization • Workaround: Parentheses 3*4 shouldBe equalTo(12) • There is no way* to avoid this entirely – Some sentences are shorter – Impossible to guarantee the odd part rule * … that I know of
  42. 42. Grammar Variance Assumption Example startWith "ScalaUA" should startWith("Scala") endWith "ScalaUA" should endWith("UA") contain List(1, 2, 3) should contain(2) equalTo 5 shouldBe greaterThan(2) Can you spot the difference?
  43. 43. Grammar Variance • We must support predicate families: – Simple modal form: List(1, 2, 3) should contain(2) – Compound subjunctive form: 3*4 shouldBe equalTo(12) • In other words, we need another verb
  44. 44. Grammar Variance • A simple solution: implicit class ValidationContext[T](data: T) { private def test(predicate: Predicate[T]): Unit = require(predicate.test(data), s"Value $data ${predicate.failure}") def shouldBe(predicate: Predicate[T]): Unit = test(predicate) def should(predicate: Predicate[T]): Unit = test(predicate) }
  45. 45. Grammar Variance • A simple solution: implicit class ValidationContext[T](data: T) { private def test(predicate: Predicate[T]): Unit = require(predicate.test(data), s"Value $data ${predicate.failure}") def shouldBe(predicate: Predicate[T]): Unit = test(predicate) def should(predicate: Predicate[T]): Unit = test(predicate) }
  46. 46. Grammar Variance • Incorrect grammar is legal: List(1, 2, 3) shouldBe contain(2) • We lack differentiation between families • First, define adequate base traits: trait ModalPredicate[-T] extends Predicate[T] trait CompoundPredicate[-T] extends Predicate[T]
  47. 47. Grammar Variance • Next, modify the verbs accordingly: def should(predicate: ModalPredicate[T])… def shouldBe(predicate: CompoundPredicate[T])… • We must also enforce the decision: – Make the base trait Predicate[T] sealed – Move it to a separate compilation unit • Finally, modify all predicates to comply
  48. 48. Negation • Negation (“not” adverb) is not just syntax – Predicate[T] must support negation – Requires a negative failure message • We must extend the model • But we must first decide on grammar
  49. 49. Negation • Modal? – "Programmers" shouldNot startWith("Java") – "Programmers" should not(startWith("Java")) • Compound? – List(1, 2, 3) shouldNotBe empty – List(1, 2, 3) shouldBe not(empty) – List(1, 2, 3) shouldNot be(empty) • Again, an aesthetic choice
  50. 50. Negation • Predicate[T] extended with negation: sealed trait Predicate[-T] { def test(data: T): Boolean def failure: String def failureNeg: String type Self[-T] <: Predicate[T] def negate: Self[T] } Negative messages Generic negation
  51. 51. Negation • Adding negation support to each family: trait ModalPredicate[-T] extends Predicate[T] { self ⇒ type Self[-T] = ModalPredicate[T] def negate = new ModalPredicate[T] { def test(data: T) = !self.test(data) def failure = self.failureNeg def failureNeg = self.failure } }
  52. 52. Negation • Finally, add the not modifier (adverb): def not[T](pred: Predicate[T]): pred.Self[T] = pred.negate • Et voilà: List(1, 2, 3) shouldBe not(empty) "Programmers" should not(startWith("Java"))
  53. 53. STEP 4: PROFIT
  54. 54. WE’RE DONE HERE! Thank you for listening tomer@tomergabel.com @tomerg http://il.linkedin.com/in/tomergabel Sample code at: http://tinyurl.com/scala-dsl-guide

Notas do Editor

  • Photo source: Renaud Camus on Flickr https://www.flickr.com/photos/renaud-camus/10940175254
  • Image source: Brooke Hoyer at Flickr, https://www.flickr.com/photos/brookehoyer/15003017355
  • Photo source: Chau Doan / World Bank at Flickr https://www.flickr.com/photos/worldbank/8880738432
  • Photo source: Pedro Moura Pinheiro at Flickr https://www.flickr.com/photos/pedromourapinheiro/5075612989
  • Photo source: Steve Corey at Flickr https://www.flickr.com/photos/stevecorey/9942526435

×