|
| 1 | +--- |
| 2 | +category: blog-detail |
| 3 | +post-type: blog |
| 4 | +by: Seth Tisue, Lightbend |
| 5 | +title: "Signature polymorphic methods in Scala" |
| 6 | +--- |
| 7 | + |
| 8 | +Java 7 introduced a curious and little-known feature to the Java |
| 9 | +Virtual Machine: "signature polymorphic" methods. These methods have |
| 10 | +strangely malleable types. |
| 11 | + |
| 12 | +This blog post explains the feature and why it exists. We also delve |
| 13 | +into how it is specified and implemented in both Scala 2 and Scala 3. |
| 14 | + |
| 15 | +The Scala 3 implementation is new, and that's the occasion for this |
| 16 | +blog post. Thanks to this recent work, **Scala 3 users can now access |
| 17 | +the entire Java reflection API**, as of Scala 3.3.0. |
| 18 | + |
| 19 | +## Should I keep reading? |
| 20 | + |
| 21 | +Signature polymorphism is admittedly an obscure feature. When you need |
| 22 | +it you need it, but the need doesn't arise in ordinary Scala |
| 23 | +code. Thus, the remaining material may be of interest primarily to JVM |
| 24 | +aficionados, Scala and Java language mavens, and compiler hackers. |
| 25 | + |
| 26 | +## When is signature polymorphism needed? |
| 27 | + |
| 28 | +Compiler support is needed when you use some portions of the Java |
| 29 | +reflection API, namely `MethodHandle` (since Java 7) and `VarHandle` |
| 30 | +(since Java 11). |
| 31 | + |
| 32 | +`MethodHandle` provides reflective access to methods on JVM classes, |
| 33 | +regardless of whether the methods were defined in Java, Scala, or some |
| 34 | +other JVM language. `VarHandle` does the same, but for fields. |
| 35 | + |
| 36 | +The polymorphism of these methods makes them more efficient, by |
| 37 | +avoiding boxing overhead when primitive values are passed, returned, |
| 38 | +or stored. |
| 39 | + |
| 40 | +## Is signature polymorphism supported in Scala? |
| 41 | + |
| 42 | +Yes: since Scala 2.11.5, and more fully since Scala 2.12.16. Scala 3 |
| 43 | +now has the support too, as of Scala 3.3.0. |
| 44 | + |
| 45 | +The initial Scala 2 implementation was done by [Jason Zaugg] in 2014 |
| 46 | +and refined later by [Lukas Rytz]. The latest version, with all fixes, |
| 47 | +landed in Scala 2.12.16 (released June 2022). |
| 48 | + |
| 49 | +Recently, [Dale Wijnand] ported the feature to Scala 3, with the |
| 50 | +assistance of [Guillaume Martres] and myself, [Seth Tisue]. |
| 51 | + |
| 52 | +Jason, Lukas, Dale, and myself are members of the Scala compiler team |
| 53 | +at [Lightbend]. We maintain Scala 2 and also contribute to Scala 3. |
| 54 | +Guillaume has worked on the Scala 3 compiler for some years, previously |
| 55 | +at [LAMP] and now at the [Scala Center]. |
| 56 | + |
| 57 | +[Jason Zaugg]: https://github.com/retronym |
| 58 | +[Lukas Rytz]: https://github.com/lrytz |
| 59 | +[Dale Wijnand]: https://github.com/dwijnand |
| 60 | +[Seth Tisue]: https://github.com/SethTisue |
| 61 | +[Guillaume Martres]: https://github.com/smarter |
| 62 | +[Lightbend]: https://lightbend.com |
| 63 | +[LAMP]: https://www.epfl.ch/labs/lamp/ |
| 64 | +[Scala Center]: https://scala.epfl.ch |
| 65 | + |
| 66 | +## What signature polymorphic methods exist? |
| 67 | + |
| 68 | +You may already have run into this feature if you have used the |
| 69 | +`MethodHandle` and `VarHandle` classes from the Java reflection API in |
| 70 | +the Java standard library. |
| 71 | + |
| 72 | +In fact, `MethodHandle` and `VarHandle` are the _only_ places you |
| 73 | +could possibly have run into this feature! |
| 74 | + |
| 75 | +That's because users are not allowed to define their own signature |
| 76 | +polymorphic methods. Only the Java standard library can do that, and |
| 77 | +so far, the creators of Java have only used the feature in these two |
| 78 | +classes. |
| 79 | + |
| 80 | +## What does "signature polymorphism" mean, exactly? |
| 81 | + |
| 82 | +There is a formal description in [JLS 15.12.3], but a more readable |
| 83 | +version is in the [Javadoc for |
| 84 | +`MethodHandle`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/invoke/MethodHandle.html). |
| 85 | +It says: |
| 86 | + |
| 87 | +> A signature polymorphic method is one which can operate with any of |
| 88 | +> a wide range of call signatures and return types. |
| 89 | +
|
| 90 | +and: |
| 91 | + |
| 92 | +> In source code, a call to a signature polymorphic method will |
| 93 | +> compile, regardless of the requested symbolic type descriptor. As |
| 94 | +> usual, the Java compiler emits an invokevirtual instruction with the |
| 95 | +> given symbolic type descriptor against the named method. The unusual |
| 96 | +> part is that the symbolic type descriptor is derived from the actual |
| 97 | +> argument and return types, not from the method declaration. |
| 98 | +
|
| 99 | +Note that generics are not sufficient to express this level of |
| 100 | +flexibility, for two reasons: |
| 101 | + |
| 102 | +First, Java generics only work on reference types, not primitive |
| 103 | +types. Scala does not have this limitation, but pays for it by |
| 104 | +incurring boxing at run-time when primitive types are used in generic |
| 105 | +contexts. |
| 106 | + |
| 107 | +Second, methods (in both languages) may only have a fixed number of |
| 108 | +type parameters, but we need one varying type for each parameter |
| 109 | +of the method we want to call reflectively. |
| 110 | + |
| 111 | +The following example should help make all of this clearer. |
| 112 | + |
| 113 | +[JLS 15.12.3]: https://docs.oracle.com/javase/specs/jls/se17/html/jls-15.html#jls-15.12.3 |
| 114 | + |
| 115 | +## How do I call a signature polymorphic method from Scala? |
| 116 | + |
| 117 | +Take `MethodHandle` for example. It provides an `invokeExact` |
| 118 | +method. Its signature as seen from Scala is: |
| 119 | + |
| 120 | + def invokeExact(args: AnyRef*): AnyRef |
| 121 | + |
| 122 | +Signature polymorphism means that the `AnyRef`s here are just |
| 123 | +placeholders for types to be supplied later. |
| 124 | + |
| 125 | +To see this work in practice, let's adapt an example from |
| 126 | +the Javadoc. From Scala, we'll make a reflective call to the `replace` |
| 127 | +method on a `String`: |
| 128 | + |
| 129 | + import java.lang.invoke._ |
| 130 | + val mt = MethodType.methodType( |
| 131 | + classOf[String], classOf[Char], classOf[Char]) |
| 132 | + val mh = MethodHandles.lookup.findVirtual( |
| 133 | + classOf[String], "replace", mt) |
| 134 | + val s = mh.invokeExact("daddy", 'd', 'n'): String |
| 135 | + |
| 136 | +If we paste this into the Scala REPL (2 or 3), we see: |
| 137 | + |
| 138 | + val s: String = nanny |
| 139 | + |
| 140 | +Signature polymorphism helped us here in two ways: |
| 141 | + |
| 142 | +* The arguments `d` and `n` will not be passed as `Object` or boxed to |
| 143 | + `java.lang.Character` at runtime, but will be passed directly as |
| 144 | + primitive `Char`s. |
| 145 | +* The result comes back as a `String` without needing to be checked |
| 146 | + or cast at runtime. |
| 147 | + |
| 148 | +## Are these methods good for anything else? |
| 149 | + |
| 150 | +Great question! |
| 151 | + |
| 152 | +Doesn't it seem puzzling that the designers of Java would go to so |
| 153 | +much trouble to make Java reflection faster? If I care so much about |
| 154 | +performance, shouldn't I avoid using reflection entirely? |
| 155 | + |
| 156 | +The real reason these methods need to be fast is to aid efficient |
| 157 | +implementation of dynamic languages on the JVM. `MethodHandle` was |
| 158 | +added to the JVM at the same time as `invokeDynamic`, as part of |
| 159 | +[JSR-292], which aimed to support efficient implementation of JRuby |
| 160 | +and other alternative JVM languages. (`invokeDynamic` is additionally |
| 161 | +used for implementing lambdas, in both Java and Scala; see [this |
| 162 | +writeup on Stack Overflow].) |
| 163 | + |
| 164 | +[JSR-292]: https://www.infoq.com/articles/invokedynamic/ |
| 165 | +[this writeup on Stack Overflow]: https://stackoverflow.com/questions/30002380/why-are-java-8-lambdas-invoked-using-invokedynamic |
| 166 | + |
| 167 | +## How is this implemented in Scala 2? |
| 168 | + |
| 169 | +Jason Zaugg describes his initial JDK 7 implementation in [PR 4139] |
| 170 | +and shows how the resulting bytecode looks. |
| 171 | + |
| 172 | +See also these well-documented followups: [PR 5594] for JDK 9, |
| 173 | +[PR 9530] for JDK 11, and [PR 9930] for JDK 17. |
| 174 | + |
| 175 | +[PR 4139]: https://github.com/scala/scala/pull/4139 |
| 176 | +[PR 5594]: https://github.com/scala/scala/pull/5594 |
| 177 | +[PR 9530]: https://github.com/scala/scala/pull/9530 |
| 178 | +[PR 9930]: https://github.com/scala/scala/pull/9930 |
| 179 | + |
| 180 | +## What's different in the Scala 3 version? |
| 181 | + |
| 182 | +We had to work harder in Scala 3 because it wasn't enough to have an |
| 183 | +an in-memory representation for signature polymorphic call sites. The |
| 184 | +call sites must also have a representation in TASTy, so we had to add |
| 185 | +a new TASTy node type. (Scala 2 pickles only represent method |
| 186 | +signatures; in contrast, TASTy represents method bodies too.) |
| 187 | + |
| 188 | +To represent a signature polymorphic call site internally, we |
| 189 | +synthesize a method type based on the types at the call site. One can |
| 190 | +imagine the original signature-polymorphic method as being infinitely |
| 191 | +overloaded, with each individual overload only being brought into |
| 192 | +existence as needed. |
| 193 | + |
| 194 | +For details, see [the pull |
| 195 | +request](https://github.com/lampepfl/dotty/pull/16225). |
| 196 | + |
| 197 | +### The path not taken |
| 198 | + |
| 199 | +Along the way we explored an alternative approach, suggested by Jason, |
| 200 | +which involved rewriting each call site to include a cast to a |
| 201 | +structural type containing an appropriately typed method. |
| 202 | + |
| 203 | +In that version, the `replace` call-site in the example above was |
| 204 | +rewritten from: |
| 205 | + |
| 206 | + mh.invokeExact("daddy", 'd', 'n'): String |
| 207 | + |
| 208 | +to: |
| 209 | + |
| 210 | + mh.asInstanceOf[ |
| 211 | + MethodHandle { |
| 212 | + def invokeExact(a0: String, a1: Char, a2: Char): String |
| 213 | + } |
| 214 | + ].invokeExact("daddy", 'd', 'n') |
| 215 | + |
| 216 | +(The actual rewrite was applied to in-memory ASTs, rather than to |
| 217 | +source code.) |
| 218 | + |
| 219 | +The transformed code could be written and read as TASTy without |
| 220 | +trouble. Later in compilation, we detected which call sites are the |
| 221 | +product of this transform, drop the cast, and emit the correct |
| 222 | +bytecode. |
| 223 | + |
| 224 | +In the end, we didn't go with this approach. As Sébastien Doeraene |
| 225 | +pointed out, although this approach avoided adding a new TASTy tag, it |
| 226 | +also gave new semantics to existing tags that older compilers wouldn't |
| 227 | +understand. Therefore the work still couldn't ship until the next |
| 228 | +minor version of the compiler. Besides, avoiding the new tag |
| 229 | +complicated the implementation. |
| 230 | + |
| 231 | +## Questions? Discussion? |
| 232 | + |
| 233 | +These are welcome on the Scala Contributors forum thread at: |
| 234 | + |
| 235 | +* (TODO Discourse link, with link back to this post) |
0 commit comments