|
| 1 | +There are two goals: |
| 2 | + |
| 3 | +- Encode anonymous functions in the same manner as Java 8 lambdas (LambdaMetafactory) without losing the benefits of specialization. |
| 4 | +- Enable Java code to treat `scala.Function1` as an functional interface |
| 5 | + |
| 6 | +## `Function1` via `LambdaMetafactory` |
| 7 | + |
| 8 | +Benefits: smaller bytecode, profit from ongoing JVM optimizations |
| 9 | +for lambda elision, inlining, etc. |
| 10 | + |
| 11 | +This requires a functional interface for FunctionN, which is a bit |
| 12 | +harder than it sounds in the face of specialized variants of apply, |
| 13 | +as well as compose/andThen. But, without changing our library at all, |
| 14 | +this still look possible! |
| 15 | + |
| 16 | +We need to create interfaces for all specialized variants of Function1. |
| 17 | +The abstract method is the specialized apply, and all other applies must |
| 18 | +forward to this. |
| 19 | + |
| 20 | +To emit smaller code, we can create a base functional interface in which |
| 21 | +the generic apply is abstract, and all of the specialized variants forward |
| 22 | +to it. This way, each specialized functional interface need only reabstract |
| 23 | +one specialized apply and redirect the unspecialized apply to it. |
| 24 | + |
| 25 | +Here's how they could look: |
| 26 | + |
| 27 | + - `scala.Function1` |
| 28 | + - [`runtime.F1`](https://github.com/retronym/java-8-function1/blob/master/src/main/java/scala/runtime/F1.java) |
| 29 | + - [`runtime.F1$mcII$sps`](https://github.com/retronym/java-8-function1/blob/master/src/main/java/scala/runtime/F1%24mcII%24sp.java) |
| 30 | + - ... (other specialized variants) |
| 31 | + |
| 32 | +We will then need to modify the backend of scalac to emit |
| 33 | +`invokedynamic` against the `LambdaMetafactory`, passing a method |
| 34 | +handle to the function-body-in-a-method that results from `-Ydelambdafy:method` |
| 35 | +lifted method body. This behaviour would be conditional on a flag, and require |
| 36 | +that you have `F1*` on the classpath at runtime. These could be shipped in a |
| 37 | +separate JAR. |
| 38 | + |
| 39 | +We could actually do all of this without needing to emit any default methods ourselves; we can simply use a code generator and javac to generate `F1*`! |
| 40 | + |
| 41 | +### Optimizer |
| 42 | + |
| 43 | +We will need to modify `GenBCodeOpt`'s to understand `indy` calls to spin |
| 44 | +up lambdas so it can still recognize opportunities for closure inlining. |
| 45 | + |
| 46 | +### Bridges |
| 47 | + |
| 48 | +Today, in Scala: |
| 49 | + |
| 50 | +``` |
| 51 | +scala> ((s: String) => s).getClass.getDeclaredMethods.mkString("\n") |
| 52 | +res2: String = |
| 53 | +public final java.lang.String $anonfun$1.apply(java.lang.String) |
| 54 | +public final java.lang.Object $anonfun$1.apply(java.lang.Object) |
| 55 | +``` |
| 56 | + |
| 57 | +In Java8, the the metafactory just spins up a class with *just* the generic |
| 58 | +signature. This is safe as the class is anonymous and only ever |
| 59 | +called through invoke-interface, so no harm done. Seems like a leaner |
| 60 | +representation. |
| 61 | + |
| 62 | +So, to emit only the generic `java.lang.Object $anonfun$1.apply(java.lang.Object)` version, |
| 63 | +we inline `java.lang.String $anonfun$1.apply(java.lang.String)` into it, |
| 64 | +so that, what ordinarly would be the bridge method, has the closure's body, |
| 65 | +with a prelude to do the unboxing. |
| 66 | + |
| 67 | +To call this `apply` method we emit `invoke-interface Object runtime.F1(Object)`. |
| 68 | +This is very close to what we currently do, except that we use Function1 as the target.) |
| 69 | + |
| 70 | +Furthermore, by *only* creating the generic signature for anonymous functions, |
| 71 | +we would avoid the rather brutal limitation imposed by erasure for value classes, [SI-6260](https://issues.scala-lang.org/browse/SI-6260). |
| 72 | + |
| 73 | +LamdaMetaFactory does have an [advanced API](http://download.java.net/jdk8/docs/api/java/lang/invoke/LambdaMetafactory.html#FLAG_BRIDGES) |
| 74 | +that allows to create additional bridges, if needed. We can also mark |
| 75 | +the closure as serializable, which should be done to be compatible |
| 76 | +with what we do today. |
| 77 | + |
| 78 | +## `scala.Function1` as a functional interface |
| 79 | + |
| 80 | +To do this, we would need to pull up the defender methods from s.r.F1 |
| 81 | +directly to the trait interface class in the standard library. We can |
| 82 | +definitely do this when we mandate Java 8. But is it safe to do it earlier? |
| 83 | + |
| 84 | +I notice that `Function.{andThen, compose}` are still generated in |
| 85 | +specialized droves, despite the attempt to mark them as @unspecialized. |
| 86 | +This looks like a bug in scalac. |
| 87 | + |
| 88 | +## `invokedynamic` calls, courtesy of javac |
| 89 | + |
| 90 | +[`Test.java`](https://github.com/retronym/java-8-function1/blob/master/src/main/java/scala/runtime/Test.java) contains Java 8 lambdas against `F1` and `F1$mcII$sp`. |
| 91 | + |
| 92 | +In the following decompilation, you can see the invokedynamic calls: |
| 93 | + |
| 94 | +``` |
| 95 | +% `java_home -v 1.8`/bin/javap -v -p -classpath target/scala-2.11.0-M7/classes Test |
| 96 | +Classfile /Users/jason/code/java-8-function1/target/scala-2.11.0-M7/classes/Test.class |
| 97 | + Last modified Jan 28, 2014; size 1890 bytes |
| 98 | + MD5 checksum 7b6665961e5a4a10440571d0fcbdd2b3 |
| 99 | + Compiled from "Test.java" |
| 100 | +public class Test |
| 101 | + SourceFile: "Test.java" |
| 102 | + InnerClasses: |
| 103 | + static #2; //class Test$1 |
| 104 | + public static final #88= #87 of #90; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles |
| 105 | + BootstrapMethods: |
| 106 | + 0: #49 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; |
| 107 | + Method arguments: |
| 108 | + #50 (Ljava/lang/Object;)Ljava/lang/Object; |
| 109 | + #51 invokestatic Test.lambda$main$0:(Ljava/lang/String;)Ljava/lang/String; |
| 110 | + #52 (Ljava/lang/String;)Ljava/lang/String; |
| 111 | + 1: #49 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; |
| 112 | + Method arguments: |
| 113 | + #50 (Ljava/lang/Object;)Ljava/lang/Object; |
| 114 | + #57 invokestatic Test.lambda$main$1:(Ljava/lang/Integer;)Ljava/lang/Integer; |
| 115 | + #58 (Ljava/lang/Integer;)Ljava/lang/Integer; |
| 116 | + 2: #49 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; |
| 117 | + Method arguments: |
| 118 | + #62 (I)I |
| 119 | + #63 invokestatic Test.lambda$main$2:(I)I |
| 120 | + #62 (I)I |
| 121 | + minor version: 0 |
| 122 | + major version: 52 |
| 123 | + flags: ACC_PUBLIC, ACC_SUPER |
| 124 | +Constant pool: |
| 125 | + ... |
| 126 | +{ |
| 127 | + public Test(); |
| 128 | + descriptor: ()V |
| 129 | + flags: ACC_PUBLIC |
| 130 | + Code: |
| 131 | + stack=1, locals=1, args_size=1 |
| 132 | + 0: aload_0 |
| 133 | + 1: invokespecial #1 // Method java/lang/Object."<init>":()V |
| 134 | + 4: return |
| 135 | + LineNumberTable: |
| 136 | + line 1: 0 |
| 137 | + LocalVariableTable: |
| 138 | + Start Length Slot Name Signature |
| 139 | + 0 5 0 this LTest; |
| 140 | +
|
| 141 | + public static void main(java.lang.String[]); |
| 142 | + descriptor: ([Ljava/lang/String;)V |
| 143 | + flags: ACC_PUBLIC, ACC_STATIC |
| 144 | + Code: |
| 145 | + stack=2, locals=4, args_size=1 |
| 146 | + 0: new #2 // class Test$1 |
| 147 | + 3: dup |
| 148 | + 4: invokespecial #3 // Method Test$1."<init>":()V |
| 149 | + 7: pop |
| 150 | + 8: invokedynamic #4, 0 // InvokeDynamic #0:apply:()Lscala/runtime/F1; |
| 151 | + 13: astore_1 |
| 152 | + 14: aload_1 |
| 153 | + 15: ldc #5 // String |
| 154 | + 17: invokeinterface #6, 2 // InterfaceMethod scala/runtime/F1.apply:(Ljava/lang/Object;)Ljava/lang/Object; |
| 155 | + 22: pop |
| 156 | + 23: invokedynamic #7, 0 // InvokeDynamic #1:apply:()Lscala/runtime/F1; |
| 157 | + 28: astore_2 |
| 158 | + 29: aload_2 |
| 159 | + 30: iconst_0 |
| 160 | + 31: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; |
| 161 | + 34: invokeinterface #6, 2 // InterfaceMethod scala/runtime/F1.apply:(Ljava/lang/Object;)Ljava/lang/Object; |
| 162 | + 39: pop |
| 163 | + 40: aload_2 |
| 164 | + 41: iconst_0 |
| 165 | + 42: invokeinterface #9, 2 // InterfaceMethod scala/runtime/F1.apply$mcII$sp:(I)I |
| 166 | + 47: pop |
| 167 | + 48: invokedynamic #10, 0 // InvokeDynamic #2:apply$mcII$sp:()Lscala/runtime/F1$mcII$sp; |
| 168 | + 53: astore_3 |
| 169 | + 54: aload_2 |
| 170 | + 55: iconst_1 |
| 171 | + 56: invokeinterface #9, 2 // InterfaceMethod scala/runtime/F1.apply$mcII$sp:(I)I |
| 172 | + 61: pop |
| 173 | + 62: aload_2 |
| 174 | + 63: iconst_1 |
| 175 | + 64: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; |
| 176 | + 67: invokeinterface #6, 2 // InterfaceMethod scala/runtime/F1.apply:(Ljava/lang/Object;)Ljava/lang/Object; |
| 177 | + 72: pop |
| 178 | + 73: return |
| 179 | + LineNumberTable: |
| 180 | + line 3: 0 |
| 181 | + line 6: 8 |
| 182 | + line 8: 14 |
| 183 | + line 10: 23 |
| 184 | + line 11: 29 |
| 185 | + line 12: 40 |
| 186 | + line 14: 48 |
| 187 | + line 16: 54 |
| 188 | + line 17: 62 |
| 189 | + line 18: 73 |
| 190 | + LocalVariableTable: |
| 191 | + Start Length Slot Name Signature |
| 192 | + 0 74 0 args [Ljava/lang/String; |
| 193 | + 14 60 1 f1 Lscala/runtime/F1; |
| 194 | + 29 45 2 f2 Lscala/runtime/F1; |
| 195 | + 54 20 3 f3 Lscala/runtime/F1$mcII$sp; |
| 196 | + LocalVariableTypeTable: |
| 197 | + Start Length Slot Name Signature |
| 198 | + 14 60 1 f1 Lscala/runtime/F1<Ljava/lang/String;Ljava/lang/String;>; |
| 199 | + 29 45 2 f2 Lscala/runtime/F1<Ljava/lang/Integer;Ljava/lang/Integer;>; |
| 200 | +
|
| 201 | + private static int lambda$main$2(int); |
| 202 | + descriptor: (I)I |
| 203 | + flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC |
| 204 | + Code: |
| 205 | + stack=1, locals=1, args_size=1 |
| 206 | + 0: iload_0 |
| 207 | + 1: ireturn |
| 208 | + LineNumberTable: |
| 209 | + line 14: 0 |
| 210 | + LocalVariableTable: |
| 211 | + Start Length Slot Name Signature |
| 212 | + 0 2 0 i I |
| 213 | +
|
| 214 | + private static java.lang.Integer lambda$main$1(java.lang.Integer); |
| 215 | + descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer; |
| 216 | + flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC |
| 217 | + Code: |
| 218 | + stack=1, locals=1, args_size=1 |
| 219 | + 0: aload_0 |
| 220 | + 1: areturn |
| 221 | + LineNumberTable: |
| 222 | + line 10: 0 |
| 223 | + LocalVariableTable: |
| 224 | + Start Length Slot Name Signature |
| 225 | + 0 2 0 i Ljava/lang/Integer; |
| 226 | +
|
| 227 | + private static java.lang.String lambda$main$0(java.lang.String); |
| 228 | + descriptor: (Ljava/lang/String;)Ljava/lang/String; |
| 229 | + flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC |
| 230 | + Code: |
| 231 | + stack=1, locals=1, args_size=1 |
| 232 | + 0: aload_0 |
| 233 | + 1: areturn |
| 234 | + LineNumberTable: |
| 235 | + line 6: 0 |
| 236 | + LocalVariableTable: |
| 237 | + Start Length Slot Name Signature |
| 238 | + 0 2 0 s Ljava/lang/String; |
| 239 | +} |
| 240 | +``` |
0 commit comments