YOU ARE DOWNLOADING DOCUMENT

Please tick the box to continue:

Transcript
Page 1: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Leveraging Scala Macros for Better

ValidationTomer Gabel, Wix

JavaOne 2014

Page 2: 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 }

Page 3: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 4: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

ENTER: ACCORD.

Page 5: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Basic Architecture

API

Combinator Library

DSL

Macro Transformation

Page 6: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

The Accord API

• Validation can succeed or fail• A failure comprises one or more

violations

sealed trait Resultcase object Success extends Resultcase class Failure(violations: Set[Violation]) extends Result

• The validator typeclass:

trait Validator[-T] extends (T ⇒ Result)

Page 7: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Why Macros?

• Quick refresher:

implicit val personValidator = validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty }

Implicit “and”

Automatic descriptiongeneration

Page 8: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Full Disclosure

Macros are experimental

Macros are hard

I will gloss over a lot of details

… and simplify a lot of things

Page 9: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 10: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Abstract Syntax Trees

def method(param: String) = param.toUpperCase

Page 11: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Abstract Syntax Trees

def method(param: String) = param.toUpperCase

Apply( Select( Ident(newTermName("param")), newTermName("toUpperCase") ), List())

Page 12: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Abstract Syntax Trees

def method(param: String) = param.toUpperCase

ValDef( Modifiers(PARAM), newTermName("param"), Select( Ident(scala.Predef), newTypeName("String") ), EmptyTree // Value)

Page 13: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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)

Page 14: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Def Macro 101

• Looks and acts like a normal functiondef radix(s: String, base: Int): Longval result = radix("2710", 16)// result == 10000L

• Two fundamental differences:– Invoked at compile time instead of

runtime– Operates on ASTs instead of values

Page 15: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Def Macro 101

• Needs a signature & implementation

def radix(s: String, base: Int): Long = macro radixImpl

def radixImpl (c: Context) (s: c.Expr[String], base: c.Expr[Int]): c.Expr[Long]

Values

ASTs

Page 16: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Def Macro 101

• What’s in a

context?

– Enclosures

(position)

– Error handling

– Logging

– Infrastructure

Page 17: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Basic Architecture

API

Combinator Library

DSL

Macro Transformation

Page 18: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Overview

implicit val personValidator = validator[Person] { p ⇒ p.firstName is notEmpty p.lastName is notEmpty }

• The validator macro:– Rewrites each rule by addition a

description– Aggregates rules with an and combinator

Macro Application

Validation Rules

Page 19: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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]]

Page 20: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Brace yourselves

Here be dragons

Page 21: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 22: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 23: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 24: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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}

Page 25: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 26: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 27: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Process Rule

• The user writes:p.firstName is notEmpty

• The compiler emits:Contextualizer(p.firstName).is(notEmpty)

Object Under Validation (OUV)

Validation

Type: Validator[_]

Page 28: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 29: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Process Rule

Contextualizer(p.firstName).is(notEmpty)

Apply

Select

Apply

TypeApply

Contextualizer

String

SelectIdent(“p”)

firstNameis

notEmpty

Page 30: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Process Rule

Contextualizer(p.firstName).is(notEmpty)

Apply

Select

Apply

TypeApply

Contextualizer

String

SelectIdent(“p”)

firstNameis

notEmpty

Page 31: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Process Rule

Apply

TypeApply

Contextualizer

String

Select

Ident(“p”)

firstName

Page 32: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Process Rule

Apply

TypeApply

Contextualizer

Φ

Select

Ident(“p”)

firstName

Page 33: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Process Rule

Apply

TypeApply

Contextualizer

Φ

Select

Ident(“p”)

firstName

Page 34: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

case Apply(TypeApply(Select(_, `term`), _), ouv :: Nil) ⇒

Process Rule

Apply

TypeApply

Contextualizer

Φ

OUV

Φ

Φ

Page 35: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 36: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 37: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

pointSelect

Ident(“p”)

firstName

validator[Person] { p ⇒ ...}

Page 38: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 39: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Generate Description

• Putting it all together:

def describeRule(rule: ValidationRule) = { val para = prototype.name val Select(Ident(`para`), description) = rule.ouv description.toString}

Page 40: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 41: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 42: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

Quasiquotes

• Provide an easy way to construct ASTs:

Apply( Select( Ident(newTermName"x"), newTermName("$plus") ), List( Ident(newTermName("y")) ))

q"x + y"

Page 43: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 44: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 45: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 46: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

The Last Mile

Page 47: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

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

Page 48: Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014.

WE’RE DONE HERE!Thank you for listening

[email protected]

@tomerg

http://il.linkedin.com/in/tomergabel

Check out Accord at:http://github.com/wix/accord


Related Documents