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

Leveraging Scala Macros for Better Validation

Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio

Confira estes a seguir

1 de 48 Anúncio

Leveraging Scala Macros for Better Validation

Baixar para ler offline

A talk given at Scalapeño 2014 and JavaOne 2014 (video links to follow).

Data validation is a common enough problem that numerous attempts have been made to solve it elegantly. The de-facto solution in Java (JSR 303) has a number of shortcomings and fails to leverage the powerful Scala type system. The release of Scala 2.10.x introduced a couple of experimental metaprogramming features, namely reflection and macros. In this talk I'll introduce macros by way of a practical example: implementing a full-blown data validation engine, utilizing def macros and a Scala DSL to enable elegant validator definition syntax and call-site.

A talk given at Scalapeño 2014 and JavaOne 2014 (video links to follow).

Data validation is a common enough problem that numerous attempts have been made to solve it elegantly. The de-facto solution in Java (JSR 303) has a number of shortcomings and fails to leverage the powerful Scala type system. The release of Scala 2.10.x introduced a couple of experimental metaprogramming features, namely reflection and macros. In this talk I'll introduce macros by way of a practical example: implementing a full-blown data validation engine, utilizing def macros and a Scala DSL to enable elegant validator definition syntax and call-site.

Anúncio
Anúncio

Mais Conteúdo rRelacionado

Diapositivos para si (18)

Semelhante a Leveraging Scala Macros for Better Validation (20)

Anúncio

Mais de Tomer Gabel (20)

Mais recentes (20)

Anúncio

Leveraging Scala Macros for Better Validation

  1. 1. Leveraging Scala Macros for Better Validation Tomer Gabel, Wix September 2014
  2. 2. I Have a Dream • Definition: case class Person( firstName: String, lastName: String ) implicit val personValidator = validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty }
  3. 3. I Have a Dream • Usage: validate(Person("Wernher", "von Braun”)) == Success validate(Person("", "No First Name”)) == Failure(Set(RuleViolation( value = "", constraint = "must not be empty", description = "firstName" )))
  4. 4. ENTER: ACCORD.
  5. 5. Basic Architecture API Combinator Library DSL Macro Transformation
  6. 6. The Accord API • Validation can succeed or fail • A failure comprises one or more violations sealed trait Result case object Success extends Result case class Failure(violations: Set[Violation]) extends Result • The validator typeclass: trait Validator[-T] extends (T ⇒ Result)
  7. 7. Why Macros? • Quick refresher: implicit val personValidator = validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty } Implicit “and” Automatic description generation
  8. 8. Full Disclosure Macros are experimental Macros are hard I will gloss over a lot of details … and simplify a lot of things
  9. 9. Abstract Syntax Trees • An intermediate representation of code – Structure (semantics) – Metadata (e.g. types) – optional! • Provided by the reflection API • Alas, mutable – Until Dotty comes along
  10. 10. Abstract Syntax Trees def method(param: String) = param.toUpperCase
  11. 11. Abstract Syntax Trees def method(param: String) = param.toUpperCase Apply( Select( Ident(newTermName("param")), newTermName("toUpperCase") ), List() )
  12. 12. Abstract Syntax Trees def method(param: String) = param.toUpperCase ValDef( Modifiers(PARAM), newTermName("param"), Select( Ident(scala.Predef), newTypeName("String") ), EmptyTree // Value )
  13. 13. Abstract Syntax Trees def method(param: String) = param.toUpperCase DefDef( Modifiers(), newTermName("method"), List(), // Type parameters List( // Parameter lists List(parameter) ), TypeTree(), // Return type implementation )
  14. 14. Def Macro 101 • Looks and acts like a normal function def radix(s: String, base: Int): Long val result = radix("2710", 16) // result == 10000L • Two fundamental differences: – Invoked at compile time instead of runtime – Operates on ASTs instead of values
  15. 15. Def Macro 101 • Needs a signature & implementation def radix(s: String, base: Int): Long = macro radixImpl def radixImpl Values (c: Context) (s: c.Expr[String], base: c.Expr[Int]): c.Expr[Long] ASTs
  16. 16. Def Macro 101 • What’s in a context? – Enclosures (position) – Error handling – Logging – Infrastructure
  17. 17. Basic Architecture API Combinator Library DSL Macro Transformation
  18. 18. Overview implicit val personValidator = validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty } • The validator macro: Macro Application Validation Rules – Rewrites each rule by addition a description – Aggregates rules with an and combinator
  19. 19. Signature def validator[T](v: T ⇒ Unit): Validator[T] = macro ValidationTransform.apply[T] def apply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒ Unit]): c.Expr[Validator[T]]
  20. 20. Brace yourselves Here be dragons
  21. 21. Walkthrough Search for rule Process rule Generate description Rewrite rule
  22. 22. Walkthrough Search for rule Process rule Generate description Rewrite rule
  23. 23. Search for Rule • A rule is an expression of type Validator[_] • We search by: – Recursively pattern matching over an AST – On match, apply a function on the subtree – Encoded as a partial function from Tree to R
  24. 24. Search for Rule def collectFromPattern[R] (tree: Tree) (pattern: PartialFunction[Tree, R]): List[R] = { var found: Vector[R] = Vector.empty new Traverser { override def traverse(subtree: Tree) { if (pattern isDefinedAt subtree) found = found :+ pattern(subtree) else super.traverse(subtree) } }.traverse(tree) found.toList }
  25. 25. Search for Rule • Putting it together: case class Rule(ouv: Tree, validation: Tree) def processRule(subtree: Tree): Rule = ??? def findRules(body: Tree): Seq[Rule] = { val validatorType = typeOf[Validator[_]] collectFromPattern(body) { case subtree if subtree.tpe <:< validatorType ⇒ processRule(subtree) } }
  26. 26. Walkthrough Search for rule Process rule Generate description Rewrite rule
  27. 27. Process Rule • The user writes: p.firstName is notEmpty • The compiler emits: Type: Validator[_] Contextualizer(p.firstName).is(notEmpty) Object Under Validation (OUV) Validation
  28. 28. Process Rule Contextualizer(p.firstName).is(notEmpty) • This is effectively an Apply AST node • The left-hand side is the OUV • The right-hand side is the validation – But we can use the entire expression! • Contextualizer is our entry point
  29. 29. Process Rule Contextualizer(p.firstName).is(notEmpty) Apply Select Apply TypeApply Contextualizer String Select Ident(“p”) firstName is notEmpty
  30. 30. Process Rule Contextualizer(p.firstName).is(notEmpty) Apply Select Apply TypeApply Contextualizer String Select Ident(“p”) firstName is notEmpty
  31. 31. Process Rule Apply TypeApply Contextualizer String Select Ident(“p”) firstName
  32. 32. Process Rule Apply TypeApply Contextualizer Φ Select Ident(“p”) firstName
  33. 33. Process Rule Apply TypeApply Contextualizer Φ Select Ident(“p”) firstName
  34. 34. Process Rule Apply TypeApply Contextualizer Φ OUV Φ Φ case Apply(TypeApply(Select(_, `term`), _), ouv :: Nil) ⇒
  35. 35. Process Rule • Putting it together: val term = newTermName("Contextualizer") def processRule(subtree: Tree): Rule = extractFromPattern(subtree) { case Apply(TypeApply(Select(_, `term`), _), ouv :: Nil) ⇒ Rule(ouv, subtree) } getOrElse abort(subtree.pos, "Not a valid rule")
  36. 36. Walkthrough Search for rule Process rule Generate description Rewrite rule
  37. 37. Generate Description Contextualizer(p.firstName).is(notEmpty) • Consider the object under validation • In this example, it is a field accessor • The function prototype is the entry point Select Ident(“p”) firstName validator[Person] { p ⇒ ... }
  38. 38. Generate Description • How to get at the prototype? • The macro signature includes the rule block: def apply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒ Unit]): c.Expr[Validator[T]] • To extract the prototype: val Function(prototype :: Nil, body) = v.tree // prototype: ValDef
  39. 39. Generate Description • Putting it all together: def describeRule(rule: ValidationRule) = { val para = prototype.name val Select(Ident(`para`), description) = rule.ouv description.toString }
  40. 40. Walkthrough Search for rule Process rule Generate description Rewrite rule
  41. 41. Rewrite Rule • We’re constructing a Validator[Person] • A rule is itself a Validator[T]. For example: Contextualizer(p.firstName).is(notEmpty) • We need to: – Lift the rule to validate the enclosing type – Apply the description to the result
  42. 42. Quasiquotes • Provide an easy way to construct ASTs: Apply( Select( Ident(newTermName"x"), newTermName("$plus") ), List( Ident(newTermName("y")) ) ) q"x + y"
  43. 43. Quasiquotes • Quasiquotes also let you splice trees: def greeting(whom: c.Expr[String]) = q"Hello "$whom"!" • And can be used in pattern matching: val q"$x + $y" = tree
  44. 44. Rewrite Rule Contextualizer(p.firstName).is(notEmpty) new Validator[Person] { def apply(p: Person) = { val validation = Contextualizer(p.firstName).is(notEmpty) validation(p.firstName) withDescription "firstName" } }
  45. 45. Rewrite Rule • Putting it all together: def rewriteRule(rule: ValidationRule) = { val desc = describeRule(rule) val tree = Literal(Constant(desc)) q""" new com.wix.accord.Validator[${weakTypeOf[T]}] { def apply($prototype) = { val validation = ${rule.validation} validation(${rule.ouv}) withDescription $tree } } """ }
  46. 46. The Last Mile
  47. 47. Epilogue • The finishing touch: and combinator def apply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒ Unit]): c.Expr[Validator[T]] = { val Function(prototype :: Nil, body) = v.tree // ... all the stuff we just discussed val rules = findRules(body) map rewriteRule val result = q"new com.wix.accord.combinators.And(..$rules)" c.Expr[Validator[T]](result) }
  48. 48. tomer@tomergabel.com @tomerg http://il.linkedin.com/in/tomergabel Check out Accord at: http://github.com/wix/accord Thank you for listening WE’RE DONE HERE!

Notas do Editor

  • Image source: http://en.wikipedia.org/wiki/File:Martin_Luther_King_-_March_on_Washington.jpg
  • Image source: https://www.flickr.com/photos/leo-gruebler/6347903993
  • Image source: https://www.flickr.com/photos/wwarby/11271811524/in/photostream/
  • Number image source: http://pixabay.com/en/count-numbers-digits-display-147393/
  • Number image source: http://pixabay.com/en/count-numbers-digits-display-147393/
  • Number image source: http://pixabay.com/en/count-numbers-digits-display-147393/
  • Number image source: http://pixabay.com/en/count-numbers-digits-display-147393/
  • Number image source: http://pixabay.com/en/count-numbers-digits-display-147393/
  • Image source: https://www.flickr.com/photos/bevgoodwin/8608320577

×