- Get rid of 23 hardwired physical function classes. One of the problems with them is that they should be effectively duplicated in reflection which means a lot of physical classes in kotlin-runtime.jar.
- Make extension functions assignable to normal functions (and vice versa), so that it's possible to do
listOfStrings.map(String::length)
- Allow functions with more than 23 parameters, theoretically any number of parameters (in practice 255 on JVM).
- At the same time, allow implementing Kotlin functions easily from Java:
new Function2() { ... }
and overridinginvoke
only would be the best. Enabling SAM conversions on Java 8 would also be terrific.
- Treat extension functions almost like non-extension functions with one extra parameter, allowing to use them almost interchangeably.
- Introduce a physical class
Function
and unlimited number of fictitious (synthetic) classesFunction0
,Function1
, ... in the compiler front-end - On JVM, introduce
Function0
..Function22
, which are optimized in a certain way, andFunctionN
for functions with 23+ parameters. When passing a lambda to Kotlin from Java, one will need to implement one of these interfaces. - Also on JVM (under the hood) add abstract
FunctionImpl
which implements all ofFunction0
..Function22
andFunctionN
(throwing exceptions), and which knows its arity. Kotlin lambdas are translated to subclasses of this abstract class, passing the correct arity to the super constructor. - Provide a way to get arity of an arbitrary
Function
object (pretty straightforward). - Hack
is/as Function5
on any numbered function in codegen (and probablyKClass.cast()
in reflection) to check againstFunction
and its arity.
Extension function type T.(P) -> R
is now just a shorthand for @ExtensionFunctionType Function2<T, P, R>
.
kotlin.extension
is a type annotation defined in built-ins.
So effectively functions and extension functions now have the same type,
which means that everything which takes a function will work with an extension function and vice versa.
To prevent unpleasant ambiguities, we introduce additional restrictions:
- A value of an extension function type cannot be called as a function, and a value of a non-extension
function type cannot be called as an extension. This requires an additional diagnostic which is only fired
when a call is resolved to the
invoke
with the wrong extension-ness. (Note that this restriction is likely to be lifted, so that extension functions can be called as functions, but not the other way around.) - Shape of a function literal argument or a function expression must exactly match
the extension-ness of the corresponding parameter. You can't pass an extension function literal
or an extension function expression where a function is expected and vice versa.
If you really want to do that, change the shape, assign literal to a variable or use the
as
operator.
So basically you can now safely coerce values between function and extension function types,
but still should invoke them in the format which you specified in their type (with or without @ExtensionFunctionType
).
With this we'll get rid of classes ExtensionFunction0
, ExtensionFunction1
, ...
and the rest of this article will deal only with usual functions.
The arity of the functional interface that the type checker can create in theory is not limited to any number, but in practice should be limited to 255 on JVM.
These interfaces are named kotlin.Function0<R>
, kotlin.Function1<P0, R>
, ..., kotlin.Function42<P0, P1, ..., P41, R>
, ...
They are fictitious, which means they have no sources and no runtime representation.
Type checker creates the corresponding descriptors on demand, IDE creates corresponding source files on demand as well.
Each of them inherits from kotlin.Function
(described below) and contains only two functions,
both of which should be synthetically produced by the compiler:
- (declaration)
invoke
with no receiver, with the corresponding number of parameters and return type. - (synthesized)
invoke
with first type parameter as the extension receiver type, and the rest as parameters and return type.
Call resolution should use the annotations on the type of the value the call is performed on
to select the correct invoke
and to report the diagnostic if the invoke
is illegal (see the previous block).
On JVM function types are erased to the physical classes defined in package kotlin.jvm.internal
:
Function0
, Function1
, ..., Function22
and FunctionN
for 23+ parameters.
There's also an empty interface kotlin.Function<R>
which is a supertype for all functions.
package kotlin
interface Function<out R>
It's a physical interface, declared in platform-agnostic built-ins, and present in kotlin-runtime.jar
for example.
However, its declaration is empty and should be empty because every physical JVM function class Function0
, Function1
, ...
inherits from it (and adds invoke()
), and we don't want to override anything besides invoke()
when doing it from Java code.
There are 23 function interfaces in kotlin.jvm.functions
: Function0
, Function1
, ..., Function22
.
Here's Function1
declaration, for example:
package kotlin.jvm.functions
interface Function1<in P1, out R> : kotlin.Function<R> {
fun invoke(p1: P1): R
}
These interfaces are supposed to be inherited from by Java classes when passing lambdas to Kotlin.
They shouldn't be used from Kotlin however, because normally you would use a function type there,
most of the time even without mentioning built-in function classes: (P1, P2, P3) -> R
.
There's also FunctionImpl
abstract class at runtime which helps in implementing arity
and vararg-invocation.
It inherits from all the physical function classes, unfortunately (more on that later).
package kotlin.jvm.internal;
// This class is implemented in Java because supertypes need to be raw classes
// for reflection to pick up correct generic signatures for inheritors
public abstract class FunctionImpl implements
Function0, Function1, ..., ..., Function22,
FunctionN // See the next section on FunctionN
{
public abstract int getArity();
@Override
public Object invoke() {
// Default implementations of all "invoke"s invoke "invokeVararg"
// This is needed for KFunctionImpl (see below)
assert getArity() == 0;
return invokeVararg();
}
@Override
public Object invoke(Object p1) {
assert getArity() == 1;
return invokeVararg(p1);
}
...
@Override
public Object invoke(Object p1, ..., Object p22) { ... }
@Override
public Object invokeVararg(Object... args) {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
// Some calculation involving generic runtime signatures
...
}
}
Each lambda is compiled to an anonymous class which inherits from FunctionImpl
and implements the corresponding invoke
:
{ (s: String): Int -> s.length }
// is translated to
object : FunctionImpl(), Function1<String, Int> {
override fun getArity(): Int = 1
/* bridge */ fun invoke(p1: Any?): Any? = ...
override fun invoke(p1: String): Int = p1.length
}
To support functions with many parameters there's a special interface in JVM runtime:
package kotlin.jvm.functions
interface FunctionN<out R> : kotlin.Function<R> {
val arity: Int
fun invokeVararg(vararg p: Any?): R
}
TODO: usual hierarchy problems: there are no such members in
kotlin.Function42
(it only hasinvoke()
), so inheritance fromFunction42
will need to be hacked somehow
And another type annotation:
package kotlin.jvm.functions
annotation class arity(val value: Int)
A lambda type with 42 parameters on JVM is translated to @arity(42) FunctionN
.
A lambda is compiled to an anonymous class which overrides invokeVararg()
instead of invoke()
:
object : FunctionImpl() {
override fun getArity(): Int = 42
override fun invokeVararg(vararg p: Any?): Any? { ... /* code */ }
// TODO: maybe assert that p's size is 42 in the beginning of invokeVararg?
}
Note that
Function0
..Function22
are provided primarily for Java interoperability and as an optimization for frequently used functions. We can change the number of functions easily from 23 to something else if we want to. For example, forKFunction
this number will be zero, since there's no point in implementing a hypotheticalKFunction5
from Java.
So when a large function is passed from Java to Kotlin, the object will need to inherit from FunctionN
:
// Kotlin
fun fooBar(f: Function42<*,*,...,*>) = f(...)
// Java
fooBar(new FunctionN<String>() {
@Override
public int getArity() { return 42; }
@Override
public String invokeVararg(Object... p) { return "42"; }
}
Note that
@arity(N) FunctionN<R>
coming from Java code will be treated as(Any?, Any?, ..., Any?) -> R
, where the number of parameters isN
. If there's no@arity
annotation on the typeFunctionN<R>
, it won't be loaded as a function type, but rather as just a classifier type with an argument.
There's an ability to get an arity of a function object and call it with variable number of arguments, provided by extensions in platform-agnostic built-ins.
package kotlin
@intrinsic val Function<*>.arity: Int
@intrinsic fun <R> Function<R>.invokeVararg(vararg p: Any?): R
But they don't have any implementation there.
The reason is, they need platform-specific function implementation to work efficiently.
This is the JVM implementation of the arity
intrinsic (invokeVararg
is essentially the same):
fun Function<*>.calculateArity(): Int {
return if (function is FunctionImpl) { // This handles the case of lambdas created from Kotlin
function.arity // Note the smart cast
}
else when (function) { // This handles all other lambdas, i.e. created from Java
is Function0 -> 0
is Function1 -> 1
...
is Function22 -> 22
is FunctionN -> function.arity // Note the smart cast
else -> throw UnsupportedOperationException() // TODO: maybe do something funny here,
// e.g. find 'invoke' reflectively
}
}
The newly introduced FunctionImpl
class inherits from all the Function0
, Function1
, ..., FunctionN
.
This means that anyLambda is Function2<*, *, *>
will be true for any Kotlin lambda.
To fix this, we need to hack is
so that it would reach out to the FunctionImpl
instance and get its arity.
package kotlin.jvm.internal
// This is the intrinsic implementation
// Calls to this function are generated by codegen on 'is' against a function type
fun isFunctionWithArity(x: Any?, n: Int): Boolean = (x as? Function).arity == n
as
should check if isFunctionWithArity(instance, arity)
, and checkcast if it is or throw exception if not.
A downside is that instanceof Function5
obviously won't work correctly from Java. We should provide a public facade to isFunctionWithArity
which should be used from Java instead of instanceof
.
Also we should issue warnings on is Array<Function2<*, *, *>>
(or as
), since it won't work for empty arrays (there's no instance of FunctionImpl
to reach out and ask the arity).
KFunction*
interfaces should be synthesized at compile-time identically to functions.
The compiler should resolve KFunction{N}
for any N
, IDEs should synthesize sources when needed,
is
/as
should be handled similarly etc.
However, we won't introduce multitudes of KFunction
s at runtime.
The two reasons we did it for Function
s were Java interop and lambda performance, and they both are not so relevant here.
A great aid was that the contents of each Function
were trivial and easy to duplicate (23-plicate?),
which is not the case at all for KFunction
s: they also contain code related to reflection.
So for reflection there will be:
- fictitious interfaces
KFunction0
,KFunction1
, ...,KFunction42
, ... (defined inkotlin.reflect
) - physical interface
KFunction
(defined inkotlin.reflect
) - physical JVM runtime implementation class
KFunctionImpl
(defined inkotlin.reflect.jvm.internal
)
As an example, KFunction1
is a fictitious interface (in much the same manner that Function1
is)
which inherits from Function1
and KFunction
. The former lets you call a type-safe invoke
on a
callable reference, and the latter allows you to use reflection features on the callable reference.
fun foo(s: String) {}
fun test() {
::foo.invoke("") // ok, calls Function1.invoke
::foo.name // ok, calls KFunction.name
}