Skip to content

Commit de3948c

Browse files
fwbrasilttim
authored andcommitted
scalding-quotation refactorings (#1761)
Address @ttim's feedback on #1755
1 parent 37c2de2 commit de3948c

File tree

3 files changed

+158
-124
lines changed

3 files changed

+158
-124
lines changed

scalding-quotation/src/main/scala/com/twitter/scalding/quotation/Liftables.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ package com.twitter.scalding.quotation
22

33
import scala.reflect.macros.blackbox.Context
44

5+
/**
6+
* These Liftables allows us to lift values into quasiquote trees.
7+
* For example:
8+
*
9+
* def test(v: Source) => q"$v"
10+
*
11+
* uses `sourceLiftable`
12+
*/
513
trait Liftables {
614
val c: Context
715
import c.universe.{ TypeName => _, _ }

scalding-quotation/src/main/scala/com/twitter/scalding/quotation/Projection.scala

Lines changed: 83 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,80 @@ case class TypeName(asString: String) extends AnyVal
88
sealed trait Projection {
99
def andThen(accessor: Accessor, typeName: TypeName): Projection =
1010
Property(this, accessor, typeName)
11+
12+
def rootProjection: TypeReference = {
13+
@tailrec def loop(p: Projection): TypeReference =
14+
p match {
15+
case p @ TypeReference(_) => p
16+
case Property(p, _, _) => loop(p)
17+
}
18+
loop(this)
19+
}
20+
21+
/**
22+
* Given a base projection, returns the projection based on it if applicable.
23+
*
24+
* For instance, given a quoted function
25+
* `val contact = Quoted.function { (c: Contact) => c.contact }`
26+
* and a call
27+
* `(p: Person) => contact(p.name)`
28+
* produces the projection
29+
* `Person.name.contact`
30+
*/
31+
def basedOn(base: Projection): Option[Projection] =
32+
this match {
33+
case TypeReference(tpe) =>
34+
base match {
35+
case TypeReference(`tpe`) => Some(base)
36+
case Property(_, _, `tpe`) => Some(base)
37+
case other => None
38+
}
39+
case Property(path, name, tpe) =>
40+
path.basedOn(base).map(Property(_, name, tpe))
41+
}
42+
43+
/**
44+
* Limits projections to only values of `superClass`. Example:
45+
*
46+
* case class Person(name: String, contact: Contact) extends ThriftObject
47+
* case class Contact(phone: Phone) extends ThriftObject
48+
* case class Phone(number: String)
49+
*
50+
* For the super class `ThriftObject`, it produces the transformations:
51+
*
52+
* Person.contact.phone => Some(Person.contact.phone)
53+
* Person.contact.phone.number => Some(Person.contact.phone)
54+
* Person.name.isEmpty => Some(Person.name)
55+
* Phone.number => None
56+
*/
57+
def bySuperClass(superClass: Class[_]): Option[Projection] = {
58+
59+
def isSubclass(c: TypeName) =
60+
try
61+
superClass.isAssignableFrom(Class.forName(c.asString))
62+
catch {
63+
case _: ClassNotFoundException =>
64+
false
65+
}
66+
67+
def loop(p: Projection): Either[Projection, Option[Projection]] =
68+
p match {
69+
case TypeReference(typeName) =>
70+
Either.cond(!isSubclass(typeName), None, p)
71+
case p @ Property(path, name, typeName) =>
72+
loop(path) match {
73+
case Left(_) =>
74+
Either.cond(!isSubclass(typeName), Some(p), p)
75+
case Right(path) =>
76+
Right(path)
77+
}
78+
}
79+
80+
loop(this) match {
81+
case Left(path) => Some(path)
82+
case Right(opt) => opt
83+
}
84+
}
1185
}
1286

1387
/**
@@ -30,81 +104,21 @@ final case class Property(path: Projection, accessor: Accessor, typeName: TypeNa
30104
final class Projections private (val set: Set[Projection]) extends Serializable {
31105

32106
/**
33-
* Returns the projections that are based on `tpe` and limits projections
107+
* Returns the projections that are based on `typeName` and limits projections
34108
* to only properties that extend from `superClass`.
35109
*/
36-
def of(typeName: TypeName, superClass: Class[_]): Projections = {
37-
38-
def byType(p: Projection) = {
39-
@tailrec def loop(p: Projection): Boolean =
40-
p match {
41-
case TypeReference(`typeName`) => true
42-
case TypeReference(_) => false
43-
case Property(p, _, _) => loop(p)
44-
}
45-
loop(p)
46-
}
47-
48-
def bySuperClass(p: Projection): Option[Projection] = {
49-
50-
def isSubclass(c: TypeName) =
51-
try
52-
superClass.isAssignableFrom(Class.forName(c.asString))
53-
catch {
54-
case _: ClassNotFoundException =>
55-
false
56-
}
57-
58-
def loop(p: Projection): Either[Projection, Option[Projection]] =
59-
p match {
60-
case TypeReference(tpe) =>
61-
Either.cond(!isSubclass(tpe), None, p)
62-
case p @ Property(path, name, tpe) =>
63-
loop(path) match {
64-
case Left(_) =>
65-
Either.cond(!isSubclass(tpe), Some(p), p)
66-
case Right(path) =>
67-
Right(path)
68-
}
69-
}
70-
71-
loop(p) match {
72-
case Left(path) => Some(path)
73-
case Right(opt) => opt
74-
}
110+
def of(typeName: TypeName, superClass: Class[_]): Projections =
111+
Projections {
112+
set.filter(_.rootProjection.typeName == typeName)
113+
.flatMap(_.bySuperClass(superClass))
75114
}
76115

77-
Projections(set.filter(byType).flatMap(bySuperClass))
78-
}
79-
80-
/**
81-
* Given a set of base projections, returns the projections based on them.
82-
*
83-
* For instance, given a quoted function
84-
* `val contact = Quoted.function { (c: Contact) => c.contact }`
85-
* and a call
86-
* `(p: Person) => contact(p.name)`
87-
* returns the projection
88-
* `Person.name.contact`
89-
*/
90-
def basedOn(base: Set[Projection]): Projections = {
91-
def loop(base: Projection, p: Projection): Option[Projection] =
92-
p match {
93-
case TypeReference(tpe) =>
94-
base match {
95-
case TypeReference(`tpe`) => Some(base)
96-
case Property(_, _, `tpe`) => Some(base)
97-
case other => None
98-
}
99-
case Property(path, name, tpe) =>
100-
loop(base, path).map(Property(_, name, tpe))
101-
}
116+
def basedOn(base: Set[Projection]): Projections =
102117
Projections {
103118
set.flatMap { p =>
104-
base.flatMap(loop(_, p))
119+
base.flatMap(p.basedOn)
105120
}
106121
}
107-
}
108122

109123
def ++(p: Projections) =
110124
Projections(set ++ p.set)
@@ -115,7 +129,7 @@ final class Projections private (val set: Set[Projection]) extends Serializable
115129
override def equals(other: Any) =
116130
other match {
117131
case other: Projections => set == other.set
118-
case other => false
132+
case other => false
119133
}
120134

121135
override def hashCode =
@@ -135,7 +149,7 @@ object Projections {
135149
p match {
136150
case Property(path, acessor, property) =>
137151
set.contains(path) || isNested(path)
138-
case _ =>
152+
case _ =>
139153
false
140154
}
141155
new Projections(set.filter(!isNested(_)))

scalding-quotation/src/main/scala/com/twitter/scalding/quotation/ProjectionMacro.scala

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,76 +26,88 @@ trait ProjectionMacro extends TreeOps with Liftables {
2626
.contains("scala.Function")
2727
}.getOrElse(false)
2828

29-
val nestedList =
30-
params.flatMap {
31-
case param @ q"(..$inputs) => $body" =>
29+
def functionBodyProjections(param: Tree, inputs: List[Tree], body: Tree): List[Tree] = {
3230

33-
val inputSymbols = inputs.map(_.symbol).toSet
31+
val inputSymbols = inputs.map(_.symbol).toSet
3432

35-
object ProjectionExtractor {
36-
def unapply(t: Tree): Option[Tree] =
37-
t match {
33+
object ProjectionExtractor {
34+
def unapply(t: Tree): Option[Tree] =
35+
t match {
3836

39-
case q"$v.$m(..$params)" => unapply(v)
37+
case q"$v.$m(..$params)" => unapply(v)
4038

41-
case q"$v.$m" if t.symbol.isMethod =>
39+
case q"$v.$m" if t.symbol.isMethod =>
4240

43-
if (inputSymbols.contains(v.symbol)) {
44-
val p =
45-
TypeReference(typeName(v))
46-
.andThen(accessor(m), typeName(t))
47-
Some(q"$p")
48-
} else
49-
unapply(v).map { n =>
50-
q"$n.andThen(${accessor(m)}, ${typeName(t)})"
51-
}
41+
if (inputSymbols.contains(v.symbol)) {
42+
val p =
43+
TypeReference(typeName(v))
44+
.andThen(accessor(m), typeName(t))
45+
Some(q"$p")
46+
} else
47+
unapply(v).map { n =>
48+
q"$n.andThen(${accessor(m)}, ${typeName(t)})"
49+
}
5250

53-
case t if inputSymbols.contains(t.symbol) =>
54-
Some(q"${TypeReference(typeName(t))}")
51+
case t if inputSymbols.contains(t.symbol) =>
52+
Some(q"${TypeReference(typeName(t))}")
5553

56-
case _ => None
57-
}
54+
case _ => None
5855
}
56+
}
5957

60-
def functionCall(func: Tree, params: List[Tree]) = {
61-
val paramProjections = params.flatMap(ProjectionExtractor.unapply)
62-
q"""
63-
$func match {
64-
case f: _root_.com.twitter.scalding.quotation.QuotedFunction =>
65-
f.quoted.projections.basedOn($paramProjections.toSet)
66-
case _ =>
67-
_root_.com.twitter.scalding.quotation.Projections(Set(..$paramProjections))
68-
}
69-
"""
58+
def functionCall(func: Tree, params: List[Tree]): Tree = {
59+
val paramProjections = params.flatMap(ProjectionExtractor.unapply)
60+
q"""
61+
$func match {
62+
case f: _root_.com.twitter.scalding.quotation.QuotedFunction =>
63+
f.quoted.projections.basedOn($paramProjections.toSet)
64+
case _ =>
65+
_root_.com.twitter.scalding.quotation.Projections(Set(..$paramProjections))
7066
}
67+
"""
68+
}
7169

72-
collect(body) {
73-
case q"$func.apply[..$t](..$params)" =>
74-
functionCall(func, params)
75-
case q"$func(..$params)" if isFunction(func) =>
76-
functionCall(func, params)
77-
case t @ ProjectionExtractor(p) =>
78-
q"_root_.com.twitter.scalding.quotation.Projections(Set($p))"
79-
}
70+
collect(body) {
71+
case q"$func.apply[..$t](..$params)" =>
72+
functionCall(func, params)
73+
case q"$func(..$params)" if isFunction(func) =>
74+
functionCall(func, params)
75+
case t @ ProjectionExtractor(p) =>
76+
q"_root_.com.twitter.scalding.quotation.Projections(Set($p))"
77+
}
78+
}
79+
80+
def functionInstanceProjections(func: Tree): List[Tree] = {
81+
val paramProjections =
82+
func.symbol.typeSignature.typeArgs.dropRight(1)
83+
.map(typeReference)
84+
q"""
85+
$func match {
86+
case f: _root_.com.twitter.scalding.quotation.QuotedFunction =>
87+
f.quoted.projections
88+
case _ =>
89+
_root_.com.twitter.scalding.quotation.Projections(Set(..$paramProjections))
90+
}
91+
""" :: Nil
92+
}
93+
94+
def methodProjections(method: Tree): List[Tree] = {
95+
val paramRefs =
96+
method.symbol.asMethod.paramLists.flatten
97+
.map(param => typeReference(param.typeSignature))
98+
q"${Projections(paramRefs.toSet)}" :: Nil
99+
}
100+
101+
val nestedList =
102+
params.flatMap {
103+
case param @ q"(..$inputs) => $body" =>
104+
functionBodyProjections(param, inputs, body)
80105

81106
case func if isFunction(func) =>
82-
val paramProjections =
83-
func.symbol.typeSignature.typeArgs.dropRight(1)
84-
.map(typeReference)
85-
q"""
86-
$func match {
87-
case f: _root_.com.twitter.scalding.quotation.QuotedFunction =>
88-
f.quoted.projections
89-
case _ =>
90-
_root_.com.twitter.scalding.quotation.Projections(Set(..$paramProjections))
91-
}
92-
""" :: Nil
107+
functionInstanceProjections(func)
93108

94109
case method if method.symbol != null && method.symbol.isMethod =>
95-
val paramRefs =
96-
method.symbol.asMethod.paramLists.flatten
97-
.map(param => typeReference(param.typeSignature))
98-
q"${Projections(paramRefs.toSet)}" :: Nil
110+
methodProjections(method)
99111

100112
case other =>
101113
Nil

0 commit comments

Comments
 (0)