1 / 48

Leveraging Scala Macros for Better Validation

Leveraging Scala Macros for Better Validation. Tomer Gabel, Wix JavaOne 2014. 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 }.

john
Download Presentation

Leveraging Scala Macros for Better Validation

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne2014

  2. I Have a Dream • Definition: caseclassPerson( firstName: String, lastName: String ) implicitvalpersonValidator= validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty }

  3. I Have a Dream • Usage: validate(Person("Wernher", "von Braun”)) == Success validate(Person("", "No First Name”)) == Failure(Set(RuleViolation( value= "", constraint = "mustnotbeempty", description = "firstName" )))

  4. Enter: Accord.

  5. Basic Architecture

  6. The Accord API • Validation can succeedor fail • A failurecomprises one or more violations sealedtraitResult caseobjectSuccessextendsResult caseclassFailure(violations: Set[Violation])extendsResult • The validatortypeclass: traitValidator[-T] extends (T ⇒Result)

  7. Why Macros? • Quick refresher: implicitvalpersonValidator= validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty } Implicit “and” Automatic description generation

  8. Full Disclosure Macros are experimental Macros are hard I will gloss over a lot of details … and simplifya lot of things

  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. Abstract Syntax Trees defmethod(param: String) = param.toUpperCase

  11. Abstract Syntax Trees defmethod(param: String) = param.toUpperCase • Apply( • Select( • Ident(newTermName("param")), • newTermName("toUpperCase") • ), • List() • )

  12. Abstract Syntax Trees defmethod(param: String) = param.toUpperCase • ValDef( • Modifiers(PARAM), • newTermName("param"), • Select( • Ident(scala.Predef), • newTypeName("String") • ), • EmptyTree// Value • )

  13. Abstract Syntax Trees defmethod(param: String) = param.toUpperCase • DefDef( • Modifiers(), • newTermName("method"), • List(), // Typeparameters • List( // Parameter lists • List(parameter) • ), • TypeTree(), // Return type • implementation • )

  14. Def Macro 101 • Looks and acts like a normal function defradix(s: String, base: Int): Long valresult = radix("2710", 16) // result == 10000L • Two fundamental differences: • Invoked at compile time instead of runtime • Operates on ASTsinstead of values

  15. Def Macro 101 • Needs a signature& implementation def radix(s: String, base: Int): Long= macro radixImpl defradixImpl (c: Context) (s: c.Expr[String], base: c.Expr[Int]): c.Expr[Long] Values ASTs

  16. Def Macro 101 • What’s in a context? • Enclosures (position) • Error handling • Logging • Infrastructure

  17. Basic Architecture

  18. Overview implicitvalpersonValidator= validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty } • The validatormacro: • Rewrites each ruleby addition a description • Aggregates rules with an and combinator Macro Application Validation Rules

  19. Signature defvalidator[T](v: T⇒ Unit): Validator[T] = macro ValidationTransform.apply[T] defapply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒Unit]): c.Expr[Validator[T]]

  20. Brace yourselves Here be dragons

  21. Walkthrough

  22. Walkthrough

  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. Search for Rule defcollectFromPattern[R] (tree: Tree) (pattern: PartialFunction[Tree, R]): List[R] = { varfound: Vector[R] = Vector.empty newTraverser { overridedeftraverse(subtree: Tree) { if(pattern isDefinedAtsubtree) found = found :+ pattern(subtree) else super.traverse(subtree) } }.traverse(tree) found.toList }

  25. Search for Rule • Putting it together: caseclassRule(ouv: Tree, validation: Tree) defprocessRule(subtree: Tree): Rule = ??? deffindRules(body: Tree): Seq[Rule] = { valvalidatorType = typeOf[Validator[_]] collectFromPattern(body) { casesubtreeifsubtree.tpe <:< validatorType ⇒ processRule(subtree) } }

  26. Walkthrough

  27. Process Rule • The user writes: p.firstName is notEmpty • The compiler emits: Contextualizer(p.firstName).is(notEmpty) Type: Validator[_] Object Under Validation (OUV) Validation

  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! • Contextualizeris our entry point

  29. Process Rule Contextualizer(p.firstName).is(notEmpty)

  30. Process Rule Contextualizer(p.firstName).is(notEmpty)

  31. Process Rule

  32. Process Rule

  33. Process Rule

  34. Process Rule caseApply(TypeApply(Select(_, `term`), _), ouv:: Nil) ⇒

  35. Process Rule • Putting it together: valterm= newTermName("Contextualizer") defprocessRule(subtree: Tree): Rule = extractFromPattern(subtree) { caseApply(TypeApply(Select(_, `term`), _), ouv:: Nil) ⇒ Rule(ouv, subtree) } getOrElseabort(subtree.pos, "Not a valid rule")

  36. Walkthrough

  37. Generate Description Contextualizer(p.firstName).is(notEmpty) • Consider the object under validation • In this example, it is a field accessor • The function prototypeis the entry point validator[Person] { p ⇒ ... }

  38. Generate Description • How to get at the prototype? • The macro signature includes the rule block: defapply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒Unit]): c.Expr[Validator[T]] • To extract the prototype: valFunction(prototype :: Nil, body) = v.tree// prototype: ValDef

  39. Generate Description • Putting it all together: defdescribeRule(rule: ValidationRule) = { valpara = prototype.name valSelect(Ident(`para`), description) = rule.ouv description.toString }

  40. Walkthrough

  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: • Liftthe rule to validate the enclosing type • Apply the descriptionto the result

  42. Quasiquotes • Provide an easy way to construct ASTs: Apply( Select( Ident(newTermName"x"), newTermName("$plus") ), List( Ident(newTermName("y")) ) ) • q"x + y"

  43. Quasiquotes • Quasiquotes also let you splicetrees: defgreeting(whom: c.Expr[String]) = q"Hello\"$whom\"!" • And can be used in pattern matching: valq"$x + $y" = tree

  44. Rewrite Rule Contextualizer(p.firstName).is(notEmpty) newValidator[Person] { defapply(p: Person) = { valvalidation = Contextualizer(p.firstName).is(notEmpty) validation(p.firstName) withDescription"firstName" } }

  45. Rewrite Rule • Putting it all together: defrewriteRule(rule: ValidationRule) = { valdesc = describeRule(rule) valtree = Literal(Constant(desc)) q""" new com.wix.accord.Validator[${weakTypeOf[T]}] { defapply($prototype) = { val validation = ${rule.validation} validation(${rule.ouv}) withDescription$tree } } """ }

  46. The Last Mile

  47. Epilogue • The finishing touch: and combinator defapply[T : c.WeakTypeTag] (c: Context) (v: c.Expr[T ⇒ Unit]): c.Expr[Validator[T]] = { valFunction(prototype :: Nil, body) = v.tree // ... all the stuff we just discussed valrules = findRules(body) map rewriteRule valresult= q"newcom.wix.accord.combinators.And(..$rules)" c.Expr[Validator[T]](result) }

  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!

More Related