bieffe62
bieffe62

Reputation: 474

Error when trying to convert a list of objects in a string using reduce function

I am playing with kotlin language and I tried the following code:

data class D( val n: Int, val s: String )
val lst = listOf( D(1,"one"), D(2, "two" ) )
val res  = lst.reduce { acc:String, d:D  ->  acc + ", " + d.toString()  }

The last statement causes the following errors:

    Expected parameter of type String
    Expected parameter of type String
    Type mismatch: inferred type is D but String was expected

while this version of the last statement works:

val res = lst.map { e -> e.toString() }.reduce {  acc, el -> acc + ", " +  el }

I do not understand why the first version does not work. The formal definition of the reduce function, found here, is the following:

inline fun <S, T : S> Iterable<T>.reduce(
    operation: (acc: S, T) -> S
): S

But this seems in contrast with the following sentence, on the same page:

Accumulates value starting with the first element and applying operation from left to right to current accumulator value and each element.

That is, as explained here:

The difference between the two functions is that fold() takes an initial value and uses it as the accumulated value on the first step, whereas the first step of reduce() uses the first and the second elements as operation arguments on the first step.

But, to be able to apply the operation on first and second element, and so on, it seems to me tha the operation shall have both arguments of the base type of the Iterable.

So, what am I missing ?

Upvotes: 0

Views: 692

Answers (3)

Adam Millerchip
Adam Millerchip

Reputation: 23091

Reduce is not the right tool here. The best function in this case is joinToString:

listOf(D(1, "one"), D(2, "two"))
    .joinToString(", ")
    .let { println(it) }

This prints:

D(n=1, s=one), D(n=2, s=two)

reduce is not designed for converting types, it's designed for reducing a collection of elements to a single element of the same type. You don't want to reduce to a single D, you want a string. You could try implementing it with fold, which is like reduce but takes an initial element you want to fold into:

listOf(D(1, "one"), D(2, "two"))
    .fold("") { acc, d -> "$acc, $d" }
    .let { println(it) }

However, this will add an extra comma:

, D(n=1, s=one), D(n=2, s=two)

Which is exactly why joinToString exists.

Upvotes: 2

Neo
Neo

Reputation: 2039

TL;DR

Your code acc:String is already a false statement inside this line:

val res  = lst.reduce { acc:String, d:D  ->  acc + ", " + d.toString()  }

Because acc can only be D, never a String! Reduce returns the same type as the Iterable it is performed on and lst is Iterable<D>.

Explanation

You already looked up the definition of reduce

inline fun <S, T : S> Iterable<T>.reduce(
    operation: (acc: S, T) -> S
): S

so lets try to put your code inside:

  1. lst is of type List<D>
  2. since List extends Iterable, we can write lst : Iterable<D>
  3. reduce will look like this now:
inline fun <D, T : D> Iterable<T>.reduce(
     operation: (acc: D, T) -> D //String is literally impossible here, because D is not a String
): S

and written out:

lst<D>.reduce { acc:D, d:D  ->  }

Upvotes: 0

Nongthonbam Tonthoi
Nongthonbam Tonthoi

Reputation: 12953

You can see the definition to understand why its not working

To make it work, you can simply create an extension function:

fun List<D>.reduce(operation: (acc: String, D) -> String): String {
    if (isEmpty())
        throw UnsupportedOperationException("Empty list can't be reduced.")
    var accumulator = this[0].toString()
    for (index in 1..lastIndex) {
        accumulator = operation(accumulator, this[index])
    }
    return accumulator
}

you can use it as:

val res  = lst.reduce { acc:String, d:D  ->  acc + ", " + d.toString()  }

or simply:

val res  = lst.reduce { acc, d -> "$acc, $d" }

You can modify the function to be more generic if you want to.

Upvotes: 1

Related Questions