Kotlin High order function signature types: matching function objects and type erasure

697 Views Asked by At

It seems that I'm missing something very important here. Kotlin function types are implemented with specific classes called FunctionN and use generics to define receiver, parameters and return type. Is there a syntax that can preserve the function signature when you store a function reference? Or any other way to perform what I'm doing in the code below?

That's because it's all fun and games, up until you wish to actualy store them to type Any, say inside a Map<String,Any>, and later retrieve them, match and downcast/assign them to a function variable and use it. The gotcha is that generics do Type Erasure, so if you stored a function to Any you can't get it back and use it, i.e. to match it to Function2<Int,Int,Int> the actual type will be: Function2<*,*,*> and is unusable because that only matches the signature to parameters of type Nothing i.e. you're done for. (The only case you can use it is for Function0 because it has no parametrers or return types.)

It seems that there's a disconnect between compile type function signatures and runtime function signatures.

I managed to overcome this with the following code, that I hate, due to the boilerplate and the fact that I have to use class inheritance to type the function signatures. I understand that casting requires the code to have extra knowledge about the object stored and could be considered an anti-pattern, but how else to store fucntions for later dynamic invocation? (there could be other data that would match parameters and types across the app/server and other modules would all declare their own functions. In the example below the classes just declare a function that is bound to the signature type, in a full fledged example the classes would take an actual lambda in the constructor i.e. each instance of Function2IntToInt points to another function, so it's 2 levels deep: signatures and function pointers.

Thank you for your kind insight and advice.

Edit: Clarification & spelling

// used as receiver object
class Zar(var v:Int) {}

// base class to represent all functions
open class Func 

// 2 function examples, all function signatures will be 1 level deep inheritance.

class Function2IntToInt : Func() {
    val func = fun Zar.(x:Int,y:Int): Int { return x+y+v}
}
//clarification: fixed example used above, could also have been:
//class Function2IntToInt(var func: Zar.(Int,Int)->Int) : Func() {}

class FunctionStringToInt : Func() {
    val func = fun (s:String): Int { return s.length}
}

fun main() {
    var a = Zar(1)
   
    // these 2 could be stored into a collection of type Map<String,Func>
    var func:Func = Function2IntToInt()
    var func2:Func = FunctionStringToInt()
    
    // function retrieval
    var func3:Func = func
    // casting. It could fail if function storage was mismatched, but not here.
    var iface = func3 as Function2IntToInt
    var f = iface.func

    println("Invocation: result must match 3: " + a.f(1,1))

}

1

There are 1 best solutions below

2
On BEST ANSWER

Any type that you store in a collection of multiple child types is going to face the same problem, thanks to the type erasure of the JVM. Casting is unavoidable in this case. And with functions, you would have to create higher-kind wrapper types like you have to be able to retrieve them by type. You can fool-proof it better by storing the items in separate lists by type, in a map where the types are keys.

val foo = mutableMapOf<Func, MutableMap<String, Func>>()

fun <T: Func> storeItem(item: T, name: String) {
  foo.getOrPut(bar::class, ::mutableMapOf)[name] = item
}

inline fun <reified T: Func> retrieveItem(name: String): T? {
  return foo[T::class]?.get(name) as T?
}

Note you don't strictly need the Func super type. You could replace it in the code above with Any and make your implementations inherit directly from their functional definitions:

abstract class FunctionStringToInt: (String) -> Int

class MyStringToIntImpl: FunctionStringToInt() {
  override fun invoke(s: String): Int {
    return s.length
  }
}