Generic High Order Function Parameter


#1

I currently have the following example code:

val functions: MutableList<(MyObject) -> Unit> = mutableListOf()

fun <T : MyObject> add(function: (T) -> Unit) {
    functions.add(function)
}

As you can see, I have a list of functions which take a parameter MyObject and I’m trying to add a function to that list. Obviously, this is a strange way to do this, but it demonstrates my point more easily than my actual complicated and messy project. This currently throws an error on functions.add(function):

    [TYPE_MISMATCH] Type mismatch.
    Required: (MyObject) -> Unit
    Found:     (T) -> Unit

I was at first confused about this because in my function generic type I say that T : MyObject. However, in reading the documentation for generics, I saw the out and in keywords which I thought would solve my problem. This issue now is, I cannot use them for my function parameter. For example:

MutableList<(out MyObject) -> Unit>

This does not compile.

My question is, how can I achieve this functionality or is it simply not possible?


#2

You appear to be trying to do something that could not work without a possible class cast exception at runtime. The compiler should not let you do this without an explicit cast or equivalent by you.

Changing MyObject in your example to Animal (to make things clearer) …

First you are defining a container for functions which take an Animal as a parameter. This means that at any stage something can call these functions with either an instance of Animal or any subclass of Animal.

Then you want to add to this list a function that takes a specific subclass of Animal (e.g. Cat), even though code elsewhere may call the function with an incompatible class (e.g. Animal or Dog or Llama). This can only be done if you are willing to handle the expected cast class exception at runtime.

Is this what you want? If so, simply change your add function to wrap the generic function it is given in another function that handles the conversion and possible cast class exception.

If this is not what you want, then perhaps you could make your code example clearer.


#3

I’m a little confused. This is what I’m trying to do (In Java, with your Animal change):

List<Consumer<? extends Animal>> functions = new ArrayList<>();

public <T extends Animal> void add(Consumer<T> function) {
    functions.add(function);
}

My problem is that with kotlin higher order functions, I cannot see any way to achieve the equivalent of Consumer<? extends Animal>. Is this the case?


#4

I see that you want to declare a container of gemeric functions. I don’t know how you intend to use that container.

Do you want to add (Cat) -> Unit to the list of functions and then later take it out of the list and call it with a Dog? Or do all the functions you add have to take any Animal (and therefore be able to be called with a Dog)?


#5

No, I would never take out a (Cat) -> Unit and pass it a Dog. In my actual code, there would be measures against this elsewhere. However, my question really isn’t about the list. The list just more clearly demonstrates my question.

As you can see, my Java example contains ? extends Animal. Is there a way to do this for the function in Kotlin?


#6

The issue is not with the variance of the function type. Parameters of function types are always covariant, so there is no need to specify out inside the function type declaration.

The actual problem is with the variance of the list element type. You have declared a list of functions that can be called with a parameter of a type MyObject. You’re trying to add to it an element of a more specific type: a function that requires a specific subclass of MyObject. This is not allowed because a MutableList is invariant; a MutableList<(MyObject) -> Unit> is not a subtype of a MutableList<(Any) -> Unit>. If this was allowed, the code in another part of your program would fail when it tried to pass a String or another type to a function that expects a MyObject.

If you’re OK with adding a cast, this will work:

class MyObject

val functions: MutableList<(MyObject) -> Unit> = mutableListOf()

fun <T : MyObject> add(function: (T) -> Unit) {
    functions.add(function as (MyObject) -> Unit)
}

#7

Consumer<? extends Animal> is rather useless covariant projection of Consumer<T>. You cannot call its accept method neither with Animal nor with any of its descendants.


#8

I realize this now but this is not my exact use case. Thanks!


#9

Thank you very much! Being new to Kotlin, I am still trying to find my way around everything. It didn’t even occur to me that I could cast between the two functions. Thanks again.


#10

@yole

Given the code you suggested …

val functions: MutableList<(MyObject) -> Unit> = mutableListOf()

fun <T : MyObject> add(function: (T) -> Unit) {
    functions.add(function as (MyObject) -> Unit)
}

… and assuming that MyObject is an open class with subclasses such as MyObject2, I don’t know how you could make use of the functions in the list.

I can add a function (MyObject2) -> Unit to the list, but this function will give a ClassCastException if it is called with MyObject. After fetching a function from the list, how would you know what you could call it with? Is reflection the only way to tell or am I missing something?

The above is somewhat academic as the example is artificial, but it raises another more important question:

Given:

val function : (MyObject2) -> Unit = ...
val castFunction = function as (MyObject) -> Unit

The cast compiles and runs, but castFunction cannot be called with MyObject. Calling it with an instance of MyObject results in java.lang.ClassCastException: MyObject cannot be cast to MyObject2. At runtime, castFunction is still a function taking MyObject2, as can be seen by executing println(castFunction).

Should such a cast be allowed by the compiler? It is essentially allowing us to pretend the function is something that it is not (as evidenced by the println and exception at runtime).


#11

You wouldn’t know what you could call it with, and even reflection won’t help (because type arguments are erased at runtime). I was assuming the OP had some way to understand this from other aspects of their code.

Casting a function to another function type is no different from casting an ArrayList<String> to an ArrayList<Int> and then getting a ClassCastException when trying to access it. Both of these casts are allowed but highlighted with an “Unchecked cast” warning.