Reputation: 2930
I am learning Kotlin coming from Java, and I stumbled upon an unexpected behavior. I noticed, that in my below code, I seem to accidentally declare a new lambda at a bad position instead of using the one I already have. How can I fix this?
I wrote these two declarations:
/**
* Dataclass used as an example.
*/
data class Meeple(var name: String, var color: String = "translucent")
/**
* Function to map from a List<T> to a new List of equal length,
* containing the ordered elements received by applying a Mapper's map
* function to every element of the input List.
*
* @param T Type of input List-elements
* @param O Type of output List-elements
* @param mapper The mapping function applied to every input element.
* @return The List of output elements received by applying the mapper on all
* input elements.
*/
fun <T, O> List<T>.map(mapper: (T) -> O?): List<O?> {
val target = ArrayList<O?>();
for (t in this) {
val mapped: O? = mapper.invoke(t)
target.add(mapped);
}
return target;
}
The data class is just a dummy example of a simple object. The List.map
extension function is meant to map from the elements of the list to a new type and return a new List of that new type, almost like a Stream.map
would in Java.
I then create some dummy Meeple
s and try to map them to their respective names:
fun main(args: Array<String>) {
val meeples = listOf(
Meeple("Jim", "#fff"),
Meeple("Cassidy"),
Meeple("David", "#f00")
)
var toFilter: String = "Cassidy"
val lambda: (Meeple) -> String? =
{ if (it.name == toFilter) null else it.name }
toFilter = "Jim"
for (name in meeples.map { lambda }) {
println(name ?: "[anonymous]") // This outputs "(Meeple) -> kotlin.String?" (x3 because of the loop)
}
}
I did this to check the behavior of the lambda, and whether it would later filter "Jim" or "Cassidy", my expectation being the later, as that was the state of toFilter
at lambda initialization.
However I got an entirely different result. The invoke
method, though described by IntelliJ as being (T) -> O?
seems to yield the name of the lambda instead of the name of the Meeple
.
It seems, that the call to meeples.map { lambda }
does not bind the lambda as I expected, but creates a new lambda, that returns lambda
and probably internally calls toString
on that as well.
How would I actually invoke the real lambda method, instead of declaring a new one?
Upvotes: 0
Views: 687
Reputation: 8315
Even though you have found the issue as you mention in comments, I am adding this answer with some details to help any future readers.
So when you create lambda using
val lambda: (Meeple) -> String? = { if (it.name == toFilter) null else it.name }
This basically translates to
final Function1 lambda = (Function1)(new Function1() {
public Object invoke(Object var1) {
return this.invoke((Meeple)var1);
}
@Nullable
public final String invoke(@NotNull Meeple it) {
Intrinsics.checkNotNullParameter(it, "it");
return Intrinsics.areEqual(it.getName(), (String)toFilter.element) ? null : it.getName();
}
});
Now correct way to pass this to your map method would be as you have mentioned in comments
name in meeples.map(lambda)
but instead of (lambda)
you wrote { lambda }
, this is the trailing lambda convention
name in meeples.map { lambda }
// if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses:
// If the lambda is the only argument in that call, the parentheses can be omitted entirely
this creates a new lambda which returns the lambda we defined above, this line basically gets translated to following
HomeFragmentKt.map(meeples, (Function1)(new Function1() {
public Object invoke(Object var1) {
return this.invoke((Meeple)var1);
}
@Nullable
public final Function1 invoke(@NotNull Meeple it) {
Intrinsics.checkNotNullParameter(it, "it");
return lambda; // It simply returns the lambda you defined, and the code to filter never gets invoked
}
}))
Upvotes: 1
Reputation: 93581
You already mentioned in the comments you figured out that you were passing a new lambda that returns your original lambda.
As for the toFilter
value changing: The lambda function is like any other interface. As you have defined it, it captures the toFilter
variable, so it will always use the current value of it when the lambda is executed. If you want to avoid capturing the variable, copy its current value into the lambda when you define the lambda. There are various ways to do this. One way is to copy it to a local variable first.
var toFilter: String = "Cassidy"
val constantToFilter = toFilter
val lambda: (Meeple) -> String? =
{ if (it.name == constantToFilter) null else it.name }
toFilter = "Jim"
Pretty much anything you can do with Stream in Java, you can do to an Iterable directly in Kotlin. The map
function is already available, as mentioned in the comments.
Edit: Since you mentioned Java behavior in the comments.
Java can capture member variables, but local variables have to be marked final
for the compiler to allow you to pass them to a lambda or interface. So in this sense they capture values only (unless you pass member variable). The equivalent to Java's final
for a local variable in Kotlin is val
.
Kotlin is more lenient than Java in this situation, and also allows you to pass a non-final local variable (var
) to an interface or lambda, and it captures the variable in this case. This is what your original code is doing.
Upvotes: 1