|
| 1 | +--- |
| 2 | +layout: overview-large |
| 3 | +title: Inference-Driving Macros |
| 4 | + |
| 5 | +disqus: true |
| 6 | + |
| 7 | +partof: macros |
| 8 | +num: 7 |
| 9 | +outof: 7 |
| 10 | +--- |
| 11 | +<span class="label important" style="float: right;">MACRO PARADISE</span> |
| 12 | + |
| 13 | +**Eugene Burmako** |
| 14 | + |
| 15 | +Inference-driving macros are pre-release features included in so-called macro paradise, an experimental branch in the official Scala repository. Follow the instructions at the ["Macro Paradise"](/overviews/macros/paradise.html) page to download and use our nightly builds. |
| 16 | + |
| 17 | +WARNING! This is the most experimental of all the experimental features in paradise. Not only the details of the API are unclear, |
| 18 | +but I'm not sure whether the current approach is the right one, unlike, for example, in type macros, where things look very solid. |
| 19 | +Your feedback is even more welcome than ever. Hint: if you scroll down, you can leave comments directly on this page. |
| 20 | + |
| 21 | +## A motivating example |
| 22 | + |
| 23 | +The use case, which gave birth to inference-driving macros, is provided by Miles Sabin and his [shapeless](https://github.com/milessabin/shapeless) library. Miles has defined the `Iso` trait, which represents isomorphisms between types. |
| 24 | + |
| 25 | + trait Iso[T, U] { |
| 26 | + def to(t : T) : U |
| 27 | + def from(u : U) : T |
| 28 | + } |
| 29 | + |
| 30 | +Currently instances of `Iso` are defined manually and then published as implicit values. Methods, which want to make use of |
| 31 | +defined isomorphisms, declare implicit parameters of type `Iso`, which then get filled in during implicit search. |
| 32 | + |
| 33 | + def foo[C](c: C)(implicit iso: Iso[C, L]): L = iso.from(c) |
| 34 | + |
| 35 | + case class Foo(i: Int, s: String, b: Boolean) |
| 36 | + implicit val fooIsoTuple = Iso.tuple(Foo.apply _, Foo.unapply _) |
| 37 | + |
| 38 | + val tp = foo(Foo(23, "foo", true)) |
| 39 | + tp : (Int, String, Boolean) |
| 40 | + tp == (23, "foo", true) |
| 41 | + |
| 42 | +As we can see, the isomorphism between a case class and a tuple is trivial. The compiler already generates the necessary methods, |
| 43 | +and we just have to make use of them. Unfortunately in Scala 2.10.0 it's impossible to simplify this even further - for every case class |
| 44 | +you have manually define an implicit `Iso` instance. |
| 45 | + |
| 46 | +Macros have proven to be quite the boilerplate scrapers, but unfortunately they can't help here in their 2.10.0 form. The problem |
| 47 | +is not just in [SI-5923](https://issues.scala-lang.org/browse/SI-5923), which has already been fixed in master for a few weeks |
| 48 | +(note to self: backport this fix to 2.10.1). |
| 49 | + |
| 50 | +The real showstopper is the fact that when typechecking applications of methods like `foo`, scalac has to infer the type argument `L`, |
| 51 | +which it has no clue about (and that's no wonder, since this is domain-specific knowledge). As a result, even if you define an implicit |
| 52 | +macro, which synthesizes `Iso[C, L]`, scalac will helpfully infer `L` as `Nothing` before expanding the macro and then everything crumbles. |
| 53 | + |
| 54 | +## Internals of type inference |
| 55 | + |
| 56 | +From what I learned about this over a few days, type inference in Scala is performed by the following two methods |
| 57 | +in `scala/tools/nsc/typechecker/Infer.scala`: [`inferExprInstance`](https://github.com/scalamacros/kepler/blob/d7b59f452f5fa35df48a5e0385f579c98ebf3555/src/compiler/scala/tools/nsc/typechecker/Infer.scala#L1123) and |
| 58 | +[`inferMethodInstance`](https://github.com/scalamacros/kepler/blob/d7b59f452f5fa35df48a5e0385f579c98ebf3555/src/compiler/scala/tools/nsc/typechecker/Infer.scala#L1173). |
| 59 | +So far I have nothing to say here other than showing `-Yinfer-debug` logs of various code snippets, which involve type inference. |
| 60 | + |
| 61 | + def foo[T1](x: T1) = ??? |
| 62 | + foo(2) |
| 63 | + |
| 64 | + [solve types] solving for T1 in ?T1 |
| 65 | + [infer method] solving for T1 in (x: T1)Nothing based on (Int)Nothing (solved: T1=Int) |
| 66 | + |
| 67 | + def bar[T2] = ??? |
| 68 | + bar |
| 69 | + |
| 70 | + [solve types] solving for T2 in ?T2 |
| 71 | + inferExprInstance { |
| 72 | + tree C.this.bar[T2] |
| 73 | + tree.tpe Nothing |
| 74 | + tparams type T2 |
| 75 | + pt ? |
| 76 | + targs Nothing |
| 77 | + tvars =?Nothing |
| 78 | + } |
| 79 | + |
| 80 | + class Baz[T] |
| 81 | + implicit val ibaz = new Baz[Int] |
| 82 | + def baz[T3](implicit ibaz: Baz[T3]) = ??? |
| 83 | + baz |
| 84 | + |
| 85 | + [solve types] solving for T3 in ?T3 |
| 86 | + inferExprInstance { |
| 87 | + tree C.this.baz[T3] |
| 88 | + tree.tpe (implicit ibaz: C.this.Baz[T3])Nothing |
| 89 | + tparams type T3 |
| 90 | + pt ? |
| 91 | + targs Nothing |
| 92 | + tvars =?Nothing |
| 93 | + } |
| 94 | + inferExprInstance/AdjustedTypeArgs { |
| 95 | + okParams |
| 96 | + okArgs |
| 97 | + leftUndet type T3 |
| 98 | + } |
| 99 | + [infer implicit] C.this.baz[T3] with pt=C.this.Baz[T3] in class C |
| 100 | + [search] C.this.baz[T3] with pt=C.this.Baz[T3] in class C, eligible: |
| 101 | + ibaz: => C.this.Baz[Int] |
| 102 | + [search] considering T3 (pt contains ?T3) trying C.this.Baz[Int] against pt=C.this.Baz[T3] |
| 103 | + [solve types] solving for T3 in ?T3 |
| 104 | + [success] found SearchResult(C.this.ibaz, TreeTypeSubstituter(List(type T3),List(Int))) for pt C.this.Baz[=?Int] |
| 105 | + [infer implicit] inferred SearchResult(C.this.ibaz, TreeTypeSubstituter(List(type T3),List(Int))) |
| 106 | + |
| 107 | + class Qwe[T] |
| 108 | + implicit def idef[T4] = new Qwe[T4] |
| 109 | + def qwe[T4](implicit xs: Qwe[T4]) = ??? |
| 110 | + qwe |
| 111 | + |
| 112 | + [solve types] solving for T4 in ?T4 |
| 113 | + inferExprInstance { |
| 114 | + tree C.this.qwe[T4] |
| 115 | + tree.tpe (implicit xs: C.this.Qwe[T4])Nothing |
| 116 | + tparams type T4 |
| 117 | + pt ? |
| 118 | + targs Nothing |
| 119 | + tvars =?Nothing |
| 120 | + } |
| 121 | + inferExprInstance/AdjustedTypeArgs { |
| 122 | + okParams |
| 123 | + okArgs |
| 124 | + leftUndet type T4 |
| 125 | + } |
| 126 | + [infer implicit] C.this.qwe[T4] with pt=C.this.Qwe[T4] in class C |
| 127 | + [search] C.this.qwe[T4] with pt=C.this.Qwe[T4] in class C, eligible: |
| 128 | + idef: [T4]=> C.this.Qwe[T4] |
| 129 | + [solve types] solving for T4 in ?T4 |
| 130 | + inferExprInstance { |
| 131 | + tree C.this.idef[T4] |
| 132 | + tree.tpe C.this.Qwe[T4] |
| 133 | + tparams type T4 |
| 134 | + pt C.this.Qwe[?] |
| 135 | + targs Nothing |
| 136 | + tvars =?Nothing |
| 137 | + } |
| 138 | + [search] considering T4 (pt contains ?T4) trying C.this.Qwe[Nothing] against pt=C.this.Qwe[T4] |
| 139 | + [solve types] solving for T4 in ?T4 |
| 140 | + [success] found SearchResult(C.this.idef[Nothing], ) for pt C.this.Qwe[=?Nothing] |
| 141 | + [infer implicit] inferred SearchResult(C.this.idef[Nothing], ) |
| 142 | + [solve types] solving for T4 in ?T4 |
| 143 | + [infer method] solving for T4 in (implicit xs: C.this.Qwe[T4])Nothing based on (C.this.Qwe[Nothing])Nothing (solved: T4=Nothing) |
| 144 | + |
| 145 | +## Proposed solution |
| 146 | + |
| 147 | +Using the infrastructure provided by [macro bundles](/overviews/macros/bundles.html) (in principle, we could achieve exactly the same |
| 148 | +thing using the traditional way of defining macro implementations, but that's not important here), we introduce the `onInfer` callback, |
| 149 | +which macros can define to be called by the compiler from `inferExprInstance` and `inferMethodInstance`. The callback takes a single |
| 150 | +parameter of type `c.TypeInferenceContext`, which encapsulates the arguments of `inferXXX` methods and provides methods to infer |
| 151 | +unknown type parameters. |
| 152 | + |
| 153 | + trait Macro { |
| 154 | + val c: Context |
| 155 | + def onInfer(tc: c.TypeInferenceContext): Unit = tc.inferDefault() |
| 156 | + } |
| 157 | + |
| 158 | + type TypeInferenceContext <: TypeInferenceContextApi |
| 159 | + trait TypeInferenceContextApi { |
| 160 | + def tree: Tree |
| 161 | + def unknowns: List[Symbol] |
| 162 | + def expectedType: Type |
| 163 | + def actualType: Type |
| 164 | + |
| 165 | + // TODO: can we get rid of this couple? |
| 166 | + def keepNothings: Boolean |
| 167 | + def useWeaklyCompatible: Boolean |
| 168 | + |
| 169 | + def infer(sym: Symbol, tpe: Type): Unit |
| 170 | + |
| 171 | + // TODO: would be lovely to have a different signature here, namely: |
| 172 | + // def inferDefault(sym: Symbol): Type |
| 173 | + // so that the macro can partially rely on out-of-the-box inference |
| 174 | + // and infer the rest afterwards |
| 175 | + def inferDefault(): Unit |
| 176 | + } |
| 177 | + |
| 178 | +With this infrastructure in place, we can write the `materializeIso` macro, which obviates the need for manual declaration of implicits. |
| 179 | +The full source code is available in [paradise/macros](https://github.com/scalamacros/kepler/blob/paradise/macros/test/files/run/macro-programmable-type-inference/Impls_Macros_1.scala), here's the relevant excerpt: |
| 180 | + |
| 181 | + override def onInfer(tic: c.TypeInferenceContext): Unit = { |
| 182 | + val C = tic.unknowns(0) |
| 183 | + val L = tic.unknowns(1) |
| 184 | + import c.universe._ |
| 185 | + import definitions._ |
| 186 | + val TypeRef(_, _, caseClassTpe :: _ :: Nil) = tic.expectedType // Iso[Test.Foo,?] |
| 187 | + tic.infer(C, caseClassTpe) |
| 188 | + val fields = caseClassTpe.typeSymbol.typeSignature.declarations.toList.collect{ case x: TermSymbol if x.isVal && x.isCaseAccessor => x } |
| 189 | + val core = (TupleClass(fields.length) orElse UnitClass).asType.toType |
| 190 | + val tequiv = if (fields.length == 0) core else appliedType(core, fields map (_.typeSignature)) |
| 191 | + tic.infer(L, tequiv) |
| 192 | + } |
0 commit comments