Skip to content

Commit 3f0b660

Browse files
authored
Merge pull request #1428 from SethTisue/signature-polymorphic-methods
2 parents ea17f85 + 260445d commit 3f0b660

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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

Comments
 (0)