Support for object construction from parameters in a map


#1

I have a library for an application where there is a need to instantiate objects from a map of the parameters.

Equivalent to the python:

class Fred:
    def __init__(self, a:str="", b:int=0):
          self.a = a
          self.b = b

parms = { "a": "text", "b": 5 }
fred = Fred(**parms)

this can be done in part using an alternate constructor, but not in a generic way. Consider:

data class DC(val str:String="",val num:Int=0){
    constructor(mapData:Map<String,Any>):this(
          mapData.get("str") as String? ?:"",
          mapData.get("num") as Int? ?: 0
    )
}

This provides a constructor that will correctly extract the parameters from a map when provided with the correct map. The problem is that there is no way providing a generic solution in place of hard coding with the actual parameter names. While this::class.constructors.last().parameters will allow me to determine the parameters of the primary constructor (is the primary constructor always the last in the list?), but as there is no way of calling the primary constructor with a parameter list that is derived from an expression, it seems there is no current way to achieve this class by inheritance or calls to method/function which would work in general.

In other words, the only current solution is lots of boilerplate, and that is not desirable.

Some equivalent to the python ** or, some building block that allows a library to provide this functionality would be very desirable.

Note: The solution has been found for the current use, through ‘callBy’ which is effectively provides an equivalent to the python ** call. This reduces the language feature request to the ability to: can we enable use of callBy to call a base constructor?


#2

I don’t know Python, but is there any guarantee that **params will retrieve parameters in the intended order?

In Kotlin, you could do:

data class DC2(val str:String="",val num:Int=0){
    constructor(mapData:Map<String,Any>):this(
            mapData.values.toTypedArray()[0] as String? ?: "",
            mapData.values.toTypedArray()[1] as Int? ?: 0
    )
}

But this would be assuming that the Map.values() would always return the values in the intended order, which is not guaranteed. What if values are returned in a different order? You get a ClassCastException.

If all parameters are of a different type (which would indeed be a good idea), you can select each one by its type. Just for the fun (but ignoring nulls to simplify the example):

data class DC2(val str:String="",val num:Int=0){

    companion object {
        operator fun invoke(mapData:Map<String,Any>): DC2 =
            mapData.values.let {
                it.filter {
                    it is String
                } + it.filter {
                    it is Int
                }
            }.let {
                DC2(it[0] as String, it[1] as Int)
            }
    }
}

I would of course never write such code for myself, except as a brain challenge.


#3

A use case is instancing data classes from nested json data.

Thank you for the response, but this example not really an improvement on the example.

The goal is not a boilerplate method created again and again within each target class, but a single generic routine within a base class or external to the class.

The problem of order of entries in the map, and even substituting missing entries with defaults and eliminating unwanted entries can be solved by transposing into a new ‘parsedMap’.

The real problem is that each parameter must be calculated from an individual expression. Not only would such a ‘parsedMap’ need to be calculated repeatedly for each parameter, but it is impossible to create a routine that solves the problem for parameter lists that differ in length.


#5

It is the way it is done in groovy. It is indeed very convenient way to create new objects. Sadly, it is strongly entangled with groovy/python dynamic nature, I can’t see a simple way to replicate it in statically typed language.

If you are keen on using this specific way of object instantiation, you probably can work with reflections. For example, create a class with empty constructor and variable properties and then assign values to each of properties with coinciding names.


#6

You may try solution based on reflection like this:

data class Data(val a:Int, val b: String)
val klas = Data::class
val cons = klas.primaryConstructor!!
val params = cons.parameters
val a = params.find { it.name == "a" }!!
val b = params.find { it.name == "b" }!!
val values = mapOf(a to 5, b to "five")
val data = cons.callBy(values)
println("$data") //Data(a=5, b=five)

Or you may try that one based on delegate properties like this:

class Data(values: MutableMap<String, Any>){
	var a:Int by values
	val b: String by values
}
val values = mutableMapOf("a" to 5, "b" to "five")
val data = Data(values)
println("Data(a=${data.a}, b=${data.b})") // Data(a=5, b=five)

More on the latter solution is on the dedicated docs page https://kotlinlang.org/docs/reference/delegated-properties.html


#7

Note the goal is not to simply create one class with this ability to be instanced, but a package where users of the package will create many classes with the ability.

The goals is how to provide the ability with least in code in each class. So an actual class would look like:

data class DC(val str:String="",val num:Int=0):BaseClass(){
    // class code goes here, but map based constructor is provided by base class
}

or at worst:

data class DC(val str:String="",val num:Int=0){
    constructor(mapData:Map<String,Any>):this( baseFunDCFunc(this) )
}

#8

You can combine Markus_M solution with extension functions over companion objects…

interface DynamicInitializer<T>

inline operator fun <reified C : Any, T : DynamicInitializer<C>> T.invoke(args: Map<String, Any>): C {
    val constructor = C::class.primaryConstructor!!
    val argmap = HashMap<KParameter, Any?>().apply {
        constructor.parameters.forEach { if (it.name in args) put(it, args[it.name]) }
    }
    return constructor.callBy(argmap)
}

Then you can use this method as follows:

data class Foo(val a: Int = 10, val b: Int = 100) {
    companion object : DynamicInitializer<Foo>
}

val foo = Foo(hashMapOf("a" to 55))

You can even go further and use this alternative:


class ObjectBuilder(private val map: MutableMap<String, Any?>) {
    infix fun String.to(any: Any?) = map.put(this, any)
}

inline operator fun <reified C : Any, T : DynamicInitializer<C>> T.invoke(build: ObjectBuilder.() -> Unit): C {
    val args = HashMap<String, Any?>().apply { ObjectBuilder(this).build() }
    val constructor = C::class.primaryConstructor!!
    val argmap = HashMap<KParameter, Any?>().apply {
        constructor.parameters.forEach { if (it.name in args) put(it, args[it.name]) }
    }
    return constructor.callBy(argmap)
}

And now you can construct object like:

val foo = Foo {
    "a" to 50
    "b" to 200
}

BTW, what you want not only throwing away type safety but also, creating objects that way is many times slower than using normal constructor invocation…


#9

Thanks @Markus_M !!

Your second solution is not at all what I seek as the goal is to have data classes which are as standard as possible.

However your first solution is almost exactly what I am after. The “cons.callBy(map)” is something I had not been able to find, and very much an equivalent of python "cons(**map).

Now the challenge is to get it working in a generic way, and I am getting close.

Thanks to your help, have the following:

inline fun <reified T>instance(vals:Map<String,Any>):T{
    val klas:KClass<Any> = T::class as KClass<Any>
    val cons = klas.primaryConstructor!!
    val valmap = cons.parameters.associateBy({it},{vals.get(it.name)})
    val data = cons.callBy(valmap) as T
    return data
}

which can be used as follows:

data class DC(val str:String="",val num:Int=0)

val testData  = instance<DC>(mapOf("str" to "six","num" to 6))
println("testData is $testData")  // testData is DC(str=six, num=6)

I still need to look at defaults, but the main hurdle for me is be able to call ‘instance’ with a variable in place ‘’ where the class is explicitly in the call. Unfortunately

val classVal = DC::class
val test = instance<classVal>(map)

does not work. I can instance a dummy instance of the class and have:

 val test = instance(dcInstance, map) 

but creating a dummy member of the class is just messy.


#10

In that case why not receive the class as an argument to instance?

fun <T: Any> instance(cls: KClass<T>, vals:Map<String,Any>):T{
    val cons = cls.primaryConstructor!!
    val valmap = cons.parameters.associateBy({it},{vals.get(it.name)})
    val data = cons.callBy(valmap)
    return data
}

inline fun <reified T: Any> instance(vals: Map<String, Any>) = instance(T::class, vals)

now you can use it with:

val classVal = DC::class
val test = instance(classVal, map)

You can also declare it as an extension function - which looks more natural:

fun <T: Any> KClass<T>.createInstance(vals:Map<String,Any>):T{
    val cons = this.primaryConstructor!!
    val valmap = cons.parameters.associateBy({it},{vals.get(it.name)})
    val data = cons.callBy(valmap)
    return data
}

//...
val test = DC::class.createInstance(map)

#11

Thank You! I am bringing code across to Kotlin, and not having not used Java for over 10 some of the nuances I am still getting. I foolishly tried that with in place of <T:Any>, so again thank you.

The extension function is not really useful as the goal is instancing objects from data received as json at run time. So objects will be instanced within the method processing the json. This code will have found the relevant class to instance by lookup from a map.

Hard to see how code which has only the class in variable can use the extension function. Also requires classes declared to describe data to also need to declare an extension function.

This is not an alternative declaration syntax within the code. Where code instances these same classes, they should be instanced the normal way. This is for instancing from within a library.

As this is in no way a replacement for normal instancing, and only for converting data received as json into object for cleaner processing, the performance is fine. However, a type mismatch between the json data what the object requires is something i will look to refine to have the best reporting the the message received was not correctly formatted.

Again, you input greatly appreciated. :slight_smile:


#12

Think about how your app will be used.
Imagine

Object foo = loadObjectFromJson( jsonString ) 

foo. ??????   

I.e. How is the caller expected to get the property names ?
If the class does not exist until runtime then the caller cannot use native static syntax like

foo.value

What is the goal of providing objects of newly constructed class types ? How is the caller expected to use them ?
There are only a very few ways – and they all involve reflection and/or serialization and have no direct syntax.
If you simply returned a map then the caller wouldn’t need to use reflection.


#13

Yes, you have nominated a use case that does not make sense. But I would be sure for every language feature it is possible to create an example that does not make sense

There was never any suggestion of use for classes not defined in the context of the caller, and I cannot imagine such a case. The use case is to instance within a library, from data in received over an interface, instances of classes that the application code has defined, and is expecting. The reflection is required because it is the application, not the library, that has defined the target classes. This use case currently occurs in our mobile apps.


#14

Since your goal here seems to be parsing Jason and you are coming from python, you may not be aware that there are already java libraries for the task. Gson and Moshi are the primary java json parsing libraries and Kotson is a Kotlin wrapper for Gson. These can already do the job of creating objects from json. Don’t try to reinvent the wheel.


#15

@dalewking - thanks for that. The original question was on an an equivalent to the python ** and the solution has been found for the current use, through ‘callBy’ which is effectively provides an equivalent to the python ** call. This reduces the language feature request to the ability to use callBy to call a base constructor.

The specific use case of converting of a library converting parsed json data to objects is another matter. Thanks for the recommendation and i will check those libraries. Generally there is a python equivalent to most java libraries but details do vary, and while none of the standard python libraries exactly matched the use case, perhaps one of these does.


#16

Also look at jackson Jackson (“FasterXML”)and its associated collection of extensions and modules.
Unless your original structured Python Objects consist only of Dictionary and List, I doubt you will find an ‘exact match’ to Python libraries – but you should be able to configure an existing Java library to read data produced by your Python code and ‘data map’ it directly into a coresponding java or kotlin object who’s type is determined at runtime (but exists statically).

Jackson-Module-Kotlin is a good ‘wrapper’ around Jackson that handles a large set of class/type differences between kotlin and Java with minimal code/overhead.