Home > Software design >  Kotlin - creating map with 3 arrays using fold not working
Kotlin - creating map with 3 arrays using fold not working

Time:11-30

I have an array of customers, each customer has properties id, uuid and subCustomer and other properties that I am not interested in. I would like to do one iteration, where I would create 3 arrays where one would hold ids, other uuids and third subcustomers only. I have tried to achieve this by using fold function like this:

customers.fold(
      mapOf(
          "ids" to listOf<String>(),
          "uuids" to listOf<UUID>(),
          "subCustomers" to listOf<String>()
      ))
      { acc, customer ->
        acc["ids"]?.plus(customer["id"])
        acc["uuids"]?.plus(customer["uuid"])
        acc["subCustomers"]?.plus(customer["subCustomer"])
      }

With this code I get an error in editor:

Type mismatch.
Required:
Map<String, List<{Comparable{String & UUID}> & java.io.Serializable}>>
Found:
List<Any?>?

I have tried this as well:

customers.fold(
      mapOf(
          "ids" to listOf<String>(),
          "uuids" to listOf<UUID>(),
          "subCustomers" to listOf<String>()
      ))
      { acc, customer ->
        mapOf(
            "ids" to acc["ids"]?.plus(customer["id"]),
            "uuids" to acc["uuids"]?.plus(customer["uuid"]),
            "subCustomers" to acc["subCustomers"]?.plus(customer["subCustomer"])
        )
      }

But, I get this errors:

Type mismatch.
Required:
List<{Comparable{String & UUID}> & java.io.Serializable}>
Found:
List<Any?>?
Type mismatch.
Required:
Map<String, List<{Comparable{String & UUID}> & java.io.Serializable}>>
Found:
Map<String, List<Any?>?>

CodePudding user response:

Write two data classes for your data. One for your customers, and one for the three lists that you want:

data class Customer(
    val id: String,
    val uuid: UUID,
    val subCustomer: String,
    )
data class CustomerDataLists(
    val ids: MutableList<String> = mutableListOf(),
    val uuids: MutableList<UUID> = mutableListOf(),
    val subCustomers: MutableList<String> = mutableListOf(),
    )

Then, just use a simple for loop to add the data in:

val dataLists = CustomerDataLists()
for (customer in customers) {
    dataLists.ids.add(customer.id)
    dataLists.uuids.add(customer.uuid)
    dataLists.subCustomers.add(customer.subCustomer)
}
// now dataLists is filled with customers' data

CodePudding user response:

@Sweeper's answer is nice. I believe in any case it's worth using data classes instead of maps for this kind of use case.

Since you don't really have any interactions between the 3 lists in the fold, you could also build those lists independently (but it's 3 iterations of course here):

data class Customer(
    val id: String,
    val uuid: UUID,
    val subCustomer: String,
)

data class AggregatedCustomers(
    val ids: List<String>,
    val uuids: List<UUID>,
    val subCustomers: List<String>,
)

val customers: List<Customer> = TODO("get that list from somewhere")

val aggregated = AggregatedCustomers(
    ids = customers.map { it.id }
    uuids = customers.map { it.uuid }
    subCustomers = customers.map { it.subCustomer }
)

CodePudding user response:

This answer contiains 3 parts:

  1. A better way to solve such problem;
  2. Why the original code doesn't work;
  3. Other problems need to pay attention.

1. A better way to solve such problem

Let's assume that the Consumer mentioned looks like this:

data class Customer(
    val id: String,
    val uuid: UUID,
    val subCustomer: String,
)

It's really not necessary to use function fold in such occasion. For loop or extension function forEach is merely enough:

val customers: List<Customer> = listOf(
    Customer("1", UUID.randomUUID(), "sub-1"),
    Customer("2", UUID.randomUUID(), "sub-2"),
    Customer("3", UUID.randomUUID(), "sub-3"),
)

val ids = mutableListOf<String>() // pay attention. use `mutableListOf` instead of `listOf()`
val uuids = mutableListOf<UUID>()
val subConsumers = mutableListOf<String>()

customers.forEach {
    ids  = it.id
    uuids  = it.uuid
    subConsumers  = it.subCustomer
}

2. Why the original code doesn't work

The proposed two pieces of code are in the same pattern:

customers.fold(
    mapOf(
        "ids" to listOf<String>(),
        "uuids" to listOf<UUID>(),
        "subCustomers" to listOf<String>()
    )
) { acc, customer ->
    // ... do something with acc and customer
}

We should first make it clear that the last statement in the fold scope is the expression to be accumulated. It's like an acc_n <combine> customer -> acc_(n 1), for each customer in customers each time, where <combine> is where we write our logic. So the first proposed piece of code doesn't work because you might not be aware that something should be returned while writing:

customers.fold(...){ acc, customer ->
    acc["ids"]?.plus(customer.id)
    acc["uuids"]?.plus(customer.uuid)
    acc["subCustomers"]?.plus(customer.subCustomer)
}

In fact, the last statement acc["subCustomers"]?.plus(...) is an expression with type List<Any>?, kotlin regard it as your "acc_(n 1)", but you propose mapOf("ids" to ...) as acc_0, which has type Map<String, ...>: not the same type as List<Any>?. And that's why you got the first error:

Type mismatch.
Required:
Map<String, List<{Comparable{String & UUID}> & java.io.Serializable}>>
Found:
List<{Comparable{String & UUID}> & java.io.Serializable}>?

We'll talk about generic types later.

Let's move on the second piece of code. A map is proposed as the last expression in the scope of fold, which is also a map:

customers.fold(...) { acc, customer ->
    mapOf(
        "ids" to acc["ids"]?.plus(customer.id),
        "uuids" to acc["uuids"]?.plus(customer.uuid),
        "subCustomers" to acc["subCustomers"]?.plus(customer.subCustomer)
    )
}

The simpliest way to eliminate error is using !! expression (not suggested!):

customers.fold(...) { acc, customer ->
    mapOf(
        "ids" to acc["ids"]?.plus(customer.id)!!,
        "uuids" to acc["uuids"]?.plus(customer.uuid)!!,
        "subCustomers" to acc["subCustomers"]?.plus(customer.subCustomer)!!
    )
}

The reason is that kotlin cannot assert acc["ids"] is not null, that's why you use ?. for a null-safe method invoke. However such invoke make the return type nullable:

val cus: Customer? = Customer("1", UUID.randomUUID(), "sub-1") // cus has type Customer? : nullable
val id1: String = cus?.id // [compile error] Type mismatch. [Required: String] [Found: String?]
val id2: String? = cus?.id // OK
val id3: String = cus?.id!! // If `cus?.id` is null, throw NPE.

You've declare acc_0 (in bracket after fold) in type Map<String, List<T>> implicitly (we will talk about T later). Just know that T is not a nullable type), but a map with type Map<String, List<T>?> was found as acc_(n 1). Types mismatch and the error was shown:

Type mismatch.
Required:
List<{Comparable{String & UUID}> & java.io.Serializable}>
Found:
List<{Comparable{String & UUID}> & java.io.Serializable}>?

3. Other problem need to pay attention

An important problem is: What's the type of acc_0?

// acc_0: 
mapOf(
    "ids" to listOf<String>(),
    "uuids" to listOf<UUID>(),
    "subCustomers" to listOf<String>()
)

Of course type of each expression on the left of to is String, and List<T> is the type of each expression on the right of it. so it must be Map<String, List<T>>. What about T? Kotlin try to find the nearest ancessor of String and UUID, and find them both implements Comparable<?> and Serializable, so that's what you see in the error. That's the type of T:

Required:
List<{Comparable{String & UUID}> & java.io.Serializable}>

This may lead to some unwanted experience:

val map = mapOf(
    "listA" to mutableListOf("233"),
    "listB" to mutableListOf(UUID.randomUUID())
)
val listA = map["A"]!! // MutableList<out {Comparable{String & UUID}> & java.io.Serializable}!>
// generic type "collapse" into `Nothing` for no type can implement both Comparable<String> and Comparable<UUID>
listA.add(Any()) // Type mismatch. [Required: Nothing] [Found: Any]

So try not to put lists with different generic type into one map.

Another problem is, when you try to invoke acc["ids"]?.plus(customer.id), you are actually invoking such method (from kotlin _Collections.kt)

public operator fun <T> Collection<T>.plus(element: T): List<T> {
    val result = ArrayList<T>(size   1)
    result.addAll(this)
    result.add(element)
    return result
}

A new list is created each time you invoke the method! Try use mutableListOf() in replace of listOf() for collections that you want to make changes, and use " =" (or ?.plusAsign() as null-safe version) operator instead. This may leads to some other problem with the original code (which is too complex to explain why), but for the code in part 1: A better way to solve such problem, the = is actually invoking:

public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
    this.add(element)
}

which just add value to list without create new ones.

  • Related