3

I am using Kotlin to parse JSON. For example, I have this representation of a country: {"code":"US", "name":"United States of America"}. To produce a Country object from such a JSONObject, I have this function:

val produceCountry = fun (js: JSONObject) =
        Country(js.getString("code"), js.getString("name"))

I can easily parse an array of Country with this function. Besides arrays of Country, however, I also have arrays of Cat, Car, Cart, CordlessPhone, etc. Each has their own produce* function transforming a JSONObject to a Kotlin object of that type. To generalize array parsing, I have this function:

fun <T> produceSetOf(array: JSONArray, element: (JSONObject) -> T): Set<T> {
    val set = mutableSetOf<T>()

    for (i in 0 until array.length())
        set.add(element(array.getJSONObject(i)))

    return set
}

So I can call produceSetOf(jsonArray, produceCountry) on encountering an array whose elements are of type Country. This works well on arrays of Cat, Car, Cart, CordlessPhone too.

Problem arises when I see an array of strings. Instead of array.getJSONObject(i), I have to use array.getString(i). In effect, I am thinking of introducing another parameterized type to the function above and have it make the call differently:

fun <S,T> produceSetOf(array: JSONArray, element: (S) -> T): Set<T> {
    val set = mutableSetOf<T>()

    for (i in 0 until array.length()) {
        when (S) {
            is String -> 
                set.add(element(array.getString(i)))
            is JSONObject ->
                set.add(element(array.getJSONObject(i)))
        }
    }

    return set
}

Of course, Kotlin does not allow me to do that. Any suggestion how I could do that while maintaining the generality of produceSetOf() and without introducing another layer of abstraction (e.g. an element iterator, or a function transforming an index into String/JSONObject)?

Thank you.

Nick Lee
  • 5,639
  • 3
  • 27
  • 35
  • 2
    You can use reifeid type parameters for this. See https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters . In your case I would add the `inline` and `reified` keywords and then check for `S::class` in your when. – avolkmann Jul 26 '18 at 09:21
  • 1
    Probably also worth mentioning https://stackoverflow.com/a/45952201/5335131 – avolkmann Jul 26 '18 at 09:26
  • @AndreasVolkmann, thank you. `inline` and `reified` do the job, EXCEPT that it now complains (at `is String`) about `Incompatible types: String and KClass`. If I put `S::class.java` in `when`, the complaint turns to `Incompatible types: String and Class`. The line `is JSONObject` does not raise complaint in either case. Any clue why that is? Thank again. – Nick Lee Jul 26 '18 at 10:10
  • Use `when (S::class) ` `String::class` instead of `is String` – avolkmann Jul 26 '18 at 10:35

1 Answers1

6

Here is one possible solution using reified type parameters.

inline fun <reified S, T> produceSetOf(array: JsonArray, element: (S) -> T): Set<T> {
    val set = mutableSetOf<T>()

    for (i in 0 until array.size()) {
        when (S::class) {
            String::class -> set.add(element(array[i].string as S))
            JsonObject::class -> set.add(element(array[i].obj as S))
        }
    }

    return set
}

val stringArray = listOf("1", "2").toJsonArray()

val stringSet = produceSetOf<String, Int>(stringArray) { it.toInt() }
println(stringSet) // prints [1, 2]

val objArray = listOf(jsonObject("key" to "value"), jsonObject("key" to "other")).toJsonArray()
val objSet = produceSetOf<JsonObject, String>(objArray) { it["key"].string }
println(objSet) // print [value, other]

I used gson for the Json objects, since I didn't know where yours were from.

A possible shorter solution:

inline fun <reified S, T> produceSetOf(array: JsonArray, element: (S) -> T): Set<T> = array.map {
    when (S::class) {
        String::class -> element(it.string as S)
        JsonObject::class -> element(it.obj as S)
        else -> throw UnsupportedOperationException("${S::class.simpleName} is not supported")
    }
}.toSet()
avolkmann
  • 2,962
  • 2
  • 19
  • 27