48
Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015

Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

  • Upload
    others

  • View
    30

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Leveraging Scala Macros for Better

Validation Tomer Gabel, Wix

March 2015

Page 2: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

ENTER: ACCORD.

Page 5: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Basic Architecture

API

Combinator Library

DSL

Macro Transformation

Page 6: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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)

Page 7: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Why Macros?

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

Implicit “and”

Automatic description generation

Page 8: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Full Disclosure

Macros are (sort of) experimental

Macros are hard

I will gloss over a lot of details

… and simplify many things

Page 9: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Abstract Syntax Trees

•  An intermediate representation of code

– Structure (semantics)

– Metadata (e.g. types) – optional!

•  Provided by the reflection API

•  Alas, mutable

– Until scala.meta comes along…?

Page 10: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Abstract Syntax Trees def  method(param:  String)  =  param.toUpperCase    

Page 11: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Abstract Syntax Trees def  method(param:  String)  =  param.toUpperCase      

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

Page 12: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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

Page 15: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Def Macro 101

•  What’s in a context?

–  Enclosures (position)

–  Error handling

–  Logging

–  Infrastructure

Page 17: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Basic Architecture

API

Combinator Library

DSL

Macro Transformation

Page 18: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Brace yourselves

Here be dragons

Page 21: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 22: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 23: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 27: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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

Apply Select

Apply

TypeApply Contextualizer

String

Select Ident(“p”)

firstName is

notEmpty

Page 30: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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

Apply Select

Apply

TypeApply Contextualizer

String

Select Ident(“p”)

firstName is

notEmpty

Page 31: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Process Rule

Apply

TypeApply Contextualizer

String

Select Ident(“p”)

firstName

Page 32: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Process Rule

Apply

TypeApply Contextualizer

Φ

Select Ident(“p”)

firstName

Page 33: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Process Rule

Apply

TypeApply Contextualizer

Φ

Select Ident(“p”)

firstName

Page 34: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Process Rule

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

Apply

TypeApply Contextualizer

Φ

OUV Φ

Φ

Page 35: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 37: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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  ⇒      ...  }

Page 38: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

Walkthrough

Search for rule

Process rule

Generate description

Rewrite rule

Page 41: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

The Last Mile

Page 47: Leveraging Scala Macros for Better Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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 Validationdownloads.typesafe.com/website/presentations/...Leveraging Scala Macros for Better Validation Tomer Gabel, Wix March 2015File Size: 7MBPage

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