|
| 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 | +language: ko |
| 11 | +--- |
| 12 | +<spanclass="label important"style="float: right;">MACRO PARADISE</span> |
| 13 | + |
| 14 | +**Eugene Burmako** |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +## A motivating example |
| 19 | + |
| 20 | +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. |
| 21 | + |
| 22 | +trait Iso[T, U]{ |
| 23 | + def to(t : T) : U |
| 24 | + def from(u : U) : T |
| 25 | +} |
| 26 | + |
| 27 | +Currently instances of `Iso` are defined manually and then published as implicit values. Methods, which want to make use of |
| 28 | +defined isomorphisms, declare implicit parameters of type `Iso`, which then get filled in during implicit search. |
| 29 | + |
| 30 | +def foo[C](c: C)(implicit iso: Iso[C, L]): L = iso.from(c) |
| 31 | + |
| 32 | +case class Foo(i: Int, s: String, b: Boolean) |
| 33 | +implicit val fooIsoTuple = Iso.tuple(Foo.apply _, Foo.unapply _) |
| 34 | + |
| 35 | +val tp = foo(Foo(23, "foo", true)) |
| 36 | +tp : (Int, String, Boolean) |
| 37 | +tp == (23, "foo", true) |
| 38 | + |
| 39 | +As we can see, the isomorphism between a case class and a tuple is trivial (actually, shapeless uses Iso's to convert between case |
| 40 | +classes and HLists, but for simplicity let's use tuples). The compiler already generates the necessary methods, |
| 41 | +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 |
| 42 | +you have manually define an implicit `Iso` instance. |
| 43 | + |
| 44 | +The real showstopper is the fact that when typechecking applications of methods like `foo`, scalac has to infer the type argument `L`, |
| 45 | +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 |
| 46 | +macro, which synthesizes `Iso[C, L]`, scalac will helpfully infer `L` as `Nothing` before expanding the macro and then everything crumbles. |
| 47 | + |
| 48 | +## The proposed solution |
| 49 | + |
| 50 | +As demonstrated by [https://github.com/scala/scala/pull/2499](https://github.com/scala/scala/pull/2499), the solution to the outlined |
| 51 | +problem is extremely simple and elegant. <spanclass="label success">NEW</span> |
| 52 | + |
| 53 | +In 2.10 we don't allow macro applications to expand until all their type arguments are inferred. However we don't have to do that. |
| 54 | +The typechecker can infer as much as it possibly can (e.g. in the running example `C` will be inferred to `Foo` and |
| 55 | +`L` will remain uninferred) and then stop. After that we expand the macro and then proceed with type inference using the type of the |
| 56 | +expansion to help the typechecker with previously undetermined type arguments. |
| 57 | + |
| 58 | +An illustration of this technique in action can be found in our [files/run/t5923c](https://github.com/scalamacros/kepler/tree/7b890f71ecd0d28c1a1b81b7abfe8e0c11bfeb71/test/files/run/t5923c) tests. |
| 59 | +Note how simple everything is. The `materializeIso` implicit macro just takes its first type argument and uses it to produce an expansion. |
| 60 | +We don't need to make sense of the second type argument (which isn't inferred yet), we don't need to interact with type inference - |
| 61 | +everything happens automatically. |
| 62 | + |
| 63 | +Please note that there is [a funny caveat](https://github.com/scalamacros/kepler/blob/7b890f71ecd0d28c1a1b81b7abfe8e0c11bfeb71/test/files/run/t5923a/Macros_1.scala) |
| 64 | +with Nothings that we plan to address later. |
| 65 | + |
| 66 | +## Internals of type inference (deprecated) |
| 67 | + |
| 68 | +From what I learned about this over a few days, type inference in Scala is performed by the following two methods |
| 69 | +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 |
| 70 | +[`inferMethodInstance`](https://github.com/scalamacros/kepler/blob/d7b59f452f5fa35df48a5e0385f579c98ebf3555/src/compiler/scala/tools/nsc/typechecker/Infer.scala#L1173). |
| 71 | +So far I have nothing to say here other than showing `-Yinfer-debug` logs of various code snippets, which involve type inference. |
| 72 | + |
| 73 | +def foo[T1](x: T1) = ??? |
| 74 | +foo(2) |
| 75 | + |
| 76 | +[solve types] solving for T1 in ?T1 |
| 77 | +[infer method] solving for T1 in (x: T1)Nothing based on (Int)Nothing (solved: T1=Int) |
| 78 | + |
| 79 | +def bar[T2] = ??? |
| 80 | +bar |
| 81 | + |
| 82 | +[solve types] solving for T2 in ?T2 |
| 83 | +inferExprInstance{ |
| 84 | + tree C.this.bar[T2] |
| 85 | + tree.tpe Nothing |
| 86 | + tparams type T2 |
| 87 | + pt ? |
| 88 | + targs Nothing |
| 89 | + tvars =?Nothing |
| 90 | +} |
| 91 | + |
| 92 | +class Baz[T] |
| 93 | +implicit val ibaz = new Baz[Int] |
| 94 | +def baz[T3](implicit ibaz: Baz[T3]) = ??? |
| 95 | +baz |
| 96 | + |
| 97 | +[solve types] solving for T3 in ?T3 |
| 98 | +inferExprInstance{ |
| 99 | + tree C.this.baz[T3] |
| 100 | + tree.tpe (implicit ibaz: C.this.Baz[T3])Nothing |
| 101 | + tparams type T3 |
| 102 | + pt ? |
| 103 | + targs Nothing |
| 104 | + tvars =?Nothing |
| 105 | +} |
| 106 | +inferExprInstance/AdjustedTypeArgs{ |
| 107 | + okParams |
| 108 | + okArgs |
| 109 | + leftUndet type T3 |
| 110 | +} |
| 111 | +[infer implicit] C.this.baz[T3] with pt=C.this.Baz[T3] in class C |
| 112 | +[search] C.this.baz[T3] with pt=C.this.Baz[T3] in class C, eligible: |
| 113 | + ibaz: => C.this.Baz[Int] |
| 114 | +[search] considering T3 (pt contains ?T3) trying C.this.Baz[Int] against pt=C.this.Baz[T3] |
| 115 | +[solve types] solving for T3 in ?T3 |
| 116 | +[success] found SearchResult(C.this.ibaz, TreeTypeSubstituter(List(type T3),List(Int))) for pt C.this.Baz[=?Int] |
| 117 | +[infer implicit] inferred SearchResult(C.this.ibaz, TreeTypeSubstituter(List(type T3),List(Int))) |
| 118 | + |
| 119 | +class Qwe[T] |
| 120 | +implicit def idef[T4] = new Qwe[T4] |
| 121 | +def qwe[T4](implicit xs: Qwe[T4]) = ??? |
| 122 | +qwe |
| 123 | + |
| 124 | +[solve types] solving for T4 in ?T4 |
| 125 | +inferExprInstance{ |
| 126 | + tree C.this.qwe[T4] |
| 127 | + tree.tpe (implicit xs: C.this.Qwe[T4])Nothing |
| 128 | + tparams type T4 |
| 129 | + pt ? |
| 130 | + targs Nothing |
| 131 | + tvars =?Nothing |
| 132 | +} |
| 133 | +inferExprInstance/AdjustedTypeArgs{ |
| 134 | + okParams |
| 135 | + okArgs |
| 136 | + leftUndet type T4 |
| 137 | +} |
| 138 | +[infer implicit] C.this.qwe[T4] with pt=C.this.Qwe[T4] in class C |
| 139 | +[search] C.this.qwe[T4] with pt=C.this.Qwe[T4] in class C, eligible: |
| 140 | + idef: [T4]=> C.this.Qwe[T4] |
| 141 | +[solve types] solving for T4 in ?T4 |
| 142 | +inferExprInstance{ |
| 143 | + tree C.this.idef[T4] |
| 144 | + tree.tpe C.this.Qwe[T4] |
| 145 | + tparams type T4 |
| 146 | + pt C.this.Qwe[?] |
| 147 | + targs Nothing |
| 148 | + tvars =?Nothing |
| 149 | +} |
| 150 | +[search] considering T4 (pt contains ?T4) trying C.this.Qwe[Nothing] against pt=C.this.Qwe[T4] |
| 151 | +[solve types] solving for T4 in ?T4 |
| 152 | +[success] found SearchResult(C.this.idef[Nothing], ) for pt C.this.Qwe[=?Nothing] |
| 153 | +[infer implicit] inferred SearchResult(C.this.idef[Nothing], ) |
| 154 | +[solve types] solving for T4 in ?T4 |
| 155 | +[infer method] solving for T4 in (implicit xs: C.this.Qwe[T4])Nothing based on (C.this.Qwe[Nothing])Nothing (solved: T4=Nothing) |
| 156 | + |
| 157 | +## Previously proposed solution (deprecated) |
| 158 | + |
| 159 | +It turns out that it's unnecessary to introduce a low-level hack in the type inference mechanism. |
| 160 | +As outlined above, there is a much more elegant and powerful solution <spanclass="label success">NEW</span>. |
| 161 | + |
| 162 | +Using the infrastructure provided by [macro bundles](/overviews/macros/bundles.html) (in principle, we could achieve exactly the same |
| 163 | +thing using the traditional way of defining macro implementations, but that's not important here), we introduce the `onInfer` callback, |
| 164 | +which macros can define to be called by the compiler from `inferExprInstance` and `inferMethodInstance`. The callback takes a single |
| 165 | +parameter of type `c.TypeInferenceContext`, which encapsulates the arguments of `inferXXX` methods and provides methods to infer |
| 166 | +unknown type parameters. |
| 167 | + |
| 168 | +trait Macro{ |
| 169 | + val c: Context |
| 170 | + def onInfer(tc: c.TypeInferenceContext): Unit = tc.inferDefault() |
| 171 | +} |
| 172 | + |
| 173 | +type TypeInferenceContext <: TypeInferenceContextApi |
| 174 | +trait TypeInferenceContextApi{ |
| 175 | + def tree: Tree |
| 176 | + def unknowns: List[Symbol] |
| 177 | + def expectedType: Type |
| 178 | + def actualType: Type |
| 179 | + |
| 180 | + // TODO: can we get rid of this couple? |
| 181 | + def keepNothings: Boolean |
| 182 | + def useWeaklyCompatible: Boolean |
| 183 | + |
| 184 | + def infer(sym: Symbol, tpe: Type): Unit |
| 185 | + |
| 186 | + // TODO: would be lovely to have a different signature here, namely: |
| 187 | + // def inferDefault(sym: Symbol): Type |
| 188 | + // so that the macro can partially rely on out-of-the-box inference |
| 189 | + // and infer the rest afterwards |
| 190 | + def inferDefault(): Unit |
| 191 | +} |
| 192 | + |
| 193 | +With this infrastructure in place, we can write the `materializeIso` macro, which obviates the need for manual declaration of implicits. |
| 194 | +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: |
| 195 | + |
| 196 | +override def onInfer(tic: c.TypeInferenceContext): Unit ={ |
| 197 | + val C = tic.unknowns(0) |
| 198 | + val L = tic.unknowns(1) |
| 199 | + import c.universe._ |
| 200 | + import definitions._ |
| 201 | + val TypeRef(_, _, caseClassTpe :: _ :: Nil) = tic.expectedType // Iso[Test.Foo,?] |
| 202 | + tic.infer(C, caseClassTpe) |
| 203 | + val fields = caseClassTpe.typeSymbol.typeSignature.declarations.toList.collect{case x: TermSymbol if x.isVal && x.isCaseAccessor => x } |
| 204 | + val core = (TupleClass(fields.length) orElse UnitClass).asType.toType |
| 205 | + val tequiv = if (fields.length == 0) core else appliedType(core, fields map (_.typeSignature)) |
| 206 | + tic.infer(L, tequiv) |
| 207 | +} |
0 commit comments