Top Banner
Leveraging Scala Macros for Better Validation Tomer Gabel, Wix JavaOne 2014
48

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

Dec 17, 2015

Download

Documents

Eugenia Nash
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
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