This doc page is specific to features shipped in Scala 2, which have either been removed in Scala 3 or replaced by an alternative. Unless otherwise stated, all the code examples in this page assume you are using Scala 2.
Denys Shabalin EXPERIMENTAL
Lifting is an extensible way to unquote custom data types in quasiquotes. Its primary use-case is support unquoting of literal values and a number of reflection primitives as trees:
scala> val two = 1 + 1
two: Int = 2
scala> val four = q"$two + $two"
four: universe.Tree = 2.$plus(2)
This code runs successfully because Int
is considered to be Liftable
by default. The Liftable
type is just a trait with a single abstract method that defines a mapping of a given type to tree:
trait Liftable[T] {
def apply(value: T): Tree
}
Whenever there is an implicit value of Liftable[T]
available, one can unquote T
in quasiquotes. This design pattern is known as a type class. You can read more about it in “Type Classes as Objects and Implicits”.
A number of data types that are supported natively by quasiquotes will never trigger the usage of a Liftable
representation, even if it's available: subtypes of Tree
, Symbol
, Name
, Modifiers
and FlagSet
.
One can also combine lifting and unquote splicing:
scala> val ints = List(1, 2, 3)
scala> val f123 = q"f(..$ints)"
f123: universe.Tree = f(1, 2, 3)
scala> val intss = List(List(1, 2, 3), List(4, 5), List(6))
scala> val f123456 = q"f(...$intss)"
f123456: universe.Tree = f(1, 2, 3)(4, 5)(6)
In this case, each element of the list will be lifted separately and the result will be spliced in at the definition point.
Bring your own
To define tree representation for your own data type just provide an implicit instance of Liftable
for it:
package points
import scala.reflect.runtime.universe._
case class Point(x: Int, y: Int)
object Point {
implicit val lift = Liftable[Point] { p =>
q"_root_.points.Point(${p.x}, ${p.y})"
}
}
This way, whenever a value of type Point
is unquoted at runtime it will be automatically transformed into a case class constructor call. In this example there are three important points you should consider:
-
The
Liftable
companion contains a helperapply
method to simplify the creation ofLiftable
instances. It takes a single type parameterT
and aT => Tree
function as a single value parameter and returns aLiftable[T]
. -
Here we only defined
Liftable
for runtime reflection. It won’t be found if you try to use it from a macro due to the fact that each universe contains its ownLiftable
, which is not compatible with the others. This problem is caused by the path-dependent nature of the current reflection API. (see sharing liftable implementation between universes) -
Due to a lack of hygiene, the reference to
Point
’s companion has to be fully qualified to ensure the correctness of this tree in every possible context. Another way to workaround this reference issue is to use symbols instead:val PointSym = symbolOf[Point].companionModule implicit val lift = Liftable[Point] { p => q"$PointSym(${p.x}, ${p.y})" }
Standard Liftables
Type | Value | Representation |
---|---|---|
Byte , Short , Int , Long |
0 |
q"0" |
Float |
0.0 |
q"0.0" |
Double |
0.0D |
q"0.0D" |
Boolean |
true , false |
q"true" , q"false" |
Char |
'c' |
q"'c'" |
Unit |
() |
q"()" |
String |
"string" |
q""" "string" """ |
Symbol |
'symbol |
q"'symbol" |
Array[T] † |
Array(1, 2) |
q"s.Array(1, 2)" ‡ |
Option[T] † |
Some(1) |
q"s.Some(1)" ‡ |
Vector[T] † |
Vector(1, 2) |
q"s.c.i.Vector(1, 2)" ‡ |
List[T] † |
List(1, 2) |
q"s.c.i.List(1, 2)" ‡ |
Map[K, V] † |
Map(1 -> 2) |
q"s.c.i.Map((1, 2))" ‡ |
Set[T] † |
Set(1, 2) |
q"s.c.i.Set(1, 2)" ‡ |
Either[L, R] † |
Left(1) |
q"s.u.Left(1)" ‡ |
TupleN[...] * † |
(1, 2) |
q"(1, 2)" |
TermName |
TermName("foo") |
q"foo" |
TypeName |
TypeName("foo") |
tq"foo" |
Tree |
tree |
tree |
Expr |
expr |
expr.tree |
Type |
typeOf[Int] |
TypeTree(typeof[Int]) |
TypeTag |
ttag |
TypeTree(ttag.tpe) |
Constant |
const |
Literal(const) |
(*) Liftable for tuples is defined for all N
in [2, 22]
range.
(†) All type parameters have to be Liftable themselves.
(‡) s.
is shorthand for scala, s.c.i.
for scala.collection.immutable
, s.u.
for scala.util.
Reusing Liftable implementation between universes
Due to the path dependent nature of the current reflection API, it is non-trivial to share the same Liftable
definition between the macro and the runtime universes. One possible way to do this is to define Liftable
implementations in a trait and instantiate it for each universe separately:
import scala.reflect.api.Universe
import scala.reflect.macros.blackbox.Context
trait LiftableImpls {
val universe: Universe
import universe._
implicit val liftPoint = Liftable[points.Point] { p =>
q"_root_.points.Point(${p.x}, ${p.y})"
}
}
object RuntimeLiftableImpls extends LiftableImpls {
val universe: universe.type = universe
}
trait MacroLiftableImpls extends LiftableImpls {
val c: Context
val universe: c.universe.type = c.universe
}
// macro impls defined as a bundle
class MyMacro(val c: Context) extends MacroLiftableImpls {
// ...
}
So, in practice, it’s much easier to just define a Liftable
for given universe at hand:
import scala.reflect.macros.blackbox.Context
// macro impls defined as a macro bundle
class MyMacros(c: Context) {
import c.universe._
implicit val liftPoint = Liftable[points.Point] { p =>
q"_root_.points.Point(${p.x}, ${p.y})"
}
// ...
}