I've been struggling making DSL to work like this. I'd like to add items inside the lambda to the mutableList inside the persons. Can anybody help with this?
persons {
Person("name")
Person("name second")
}
the expected result after the lambda executed, all those item will be put inside the mutableList like this:
mutableListOf(Person("name"), Person("name second"))
Assuming that Person is a:
data class Person(val name: String)
Then the line Person("name") does nothing - it just desclares an unused instance. Person("name second") does the same (generally speaking, as it is the last line in lambda, it implicitly returned as the result of lambda expsession and theoretically, it could be handled later; anyway, that DSL syntax won't be working in general case).
You need not just declare instances, but also add them to list. So you need to declare some auxilary function (person for instance, to be close to desired syntax) which will do this under the hood:
class Persons {
val delegate: MutableList<Person> = mutableListOf()
fun person(name: String, block: Person.() -> Unit = {}) {
delegate.add(Person(name).also(block))
}
}
fun persons(block: Persons.() -> Unit) = Persons().also(block).delegate.toList() //mutation was needed only during construction, afterwards consider preserving immutability
Usage:
val persons: List<Person> = persons {
person("name")
person("name second")
}
Not quite exactly as you have but should be able to use something like
data class Person(var name: String? = null)
class Persons : ArrayList<Person>() {
fun person(block: Person.() -> Unit) {
val person = Person().apply(block)
add(person)
}
}
fun persons(block : Persons.() -> Unit): Persons = Persons().apply(block)
fun main() {
val personList = persons {
person {
name = "John"
}
person {
name = "Jane"
}
}
println(personList)
}
(This could be expanded then to use some kind of builder pattern to allow use of immutable vals in the data class)
Related
I have a situation where I need to create a copy of data class object. I don't know in advance which of the many data classes I have will come in into the function. I do know, however, that only data classes will be used as input to this function.
This is what didn't work:
fun doSomething(obj: Any): Any {
obj.copy(...) // <- there's no 'copy' on Any
...
}
This is what I really like to do:
fun doSomething(obj: KAnyDataClass): KAnyDataClass {
obj.copy(...) // <- works, data classes have a 'copy' method
...
}
I'm not a Kotlin developer, but it looks like the language does not support dynamic dispatch or traits. You might find success with the dynamic type, which just turns off the type-checker so it won't yell at you for using a method that it doesn't know about. However this opens up the possibility of a runtime error if you pass an argument that actually doesn't have that method.
There is no class or interface for data classes, but we know from the documentation of data classes that there are derived functions componentN and copy in each data class.
We can use that knowledge to write an abstract copy method that calls the copy method of a given arbitrary data class using reflection:
fun <T : Any> copy(data: T, vararg override: Pair<Int, Any?>): T {
val kClass = data::class
if (!kClass.isData) error("expected a data class")
val copyFun = kClass.functions.first { it.name == "copy" }
checkParameters(override, kClass)
val vals = determineComponentValues(copyFun, kClass, override, data)
#Suppress("UNCHECKED_CAST")
return copyFun.call(data, *vals) as T
}
/** check if override of parameter has the right type and nullability */
private fun <T : Any> checkParameters(
override: Array<out Pair<Int, Any?>>,
kClass: KClass<out T>
) {
override.forEach { (index, value) ->
val expectedType = kClass.functions.first { it.name == "component${index + 1}" }.returnType
if (value == null) {
if (!kClass.functions.first { it.name == "component${index + 1}" }.returnType.isMarkedNullable) {
error("value for parameter $index is null but parameter is not nullable")
}
} else {
if (!expectedType.jvmErasure.isSuperclassOf(value::class))
error("wrong type for parameter $index: expected $expectedType but was ${value::class}")
}
}
}
/** determine for each componentN the value from override or data element */
private fun <T : Any> determineComponentValues(
copyFun: KFunction<*>,
kClass: KClass<out T>,
override: Array<out Pair<Int, Any?>>,
data: T
): Array<Any?> {
val vals = (1 until copyFun.parameters.size)
.map { "component$it" }
.map { name -> kClass.functions.first { it.name == name } }
.mapIndexed { index, component ->
override.find { it.first == index }.let { if (it !== null) it.second else component.call(data) }
}
.toTypedArray()
return vals
}
Since this copy function is generic and not for a specific data class, it is not possible to specify overloads in the usual way, but I tried to support it in another way.
Let's say we have a data class and element
data class Example(
val a: Int,
val b: String,
)
val example: Any = Example(1, "x")
We can create a copy of example with copy(example) that has the same elements as the original.
If we want to override the first element, we cannot write copy(example, a = 2), but we can write copy(example, 0 to 2), saying that we want to override the first component with value 2.
Analogously we can write copy(example, 0 to 3, 1 to "y") to specify that we want to change the first and the second component.
I am not sure if this works for all cases since I just wrote it, but it should be a good start to work with.
The goal is to store a list of generic objects (of different type argument) in a list and operate on them in a type safe and ergonomic way.
My current design uses a visitor pattern:
sealed interface Element {
fun visit(visitor: Visitor)
}
interface Visitor {
fun <T> accept(elementA: ElementA<T>)
}
data class ElementA<T>(
// example members, many more in the real code
val produce : () -> T,
val doSomething: (T) -> Unit
) : Element {
override fun visit(visitor: Visitor) = visitor.accept(this)
}
Nevertheless the visitor is not very convenient to write as one has to subclass Visitor:
fun exampleUse(elements: List<Element>) {
for (element in elements) {
element.visit(object : Visitor {
override fun <T> accept(elementA: ElementA<T>) {
// We don't care about the actual type T, just that it exists
elementA.doSomething(elementA.produce())
}
})
}
}
It's not very ergonomic, and I would like for users to only have to write conventional and short code like
element.visit {it.doSomething(it.produce())}.
The only requirements are that:
the elements should be storable in a list (homogeneous collection)
the usage (eg: a.doSomething(a.produce())) should kept be separated from the class as they are defined in different package with different concerns.
If there is a way to avoid the visitor boilerplate, it's even better.
This seems to satisfy your requirements (I am assuming Element exists only as a common supertype for all ElementA<Something>):
data class ElementA<T>(
// example members, many more in the real code
val produce : () -> T,
val doSomething: (T) -> Unit
)
// optional
typealias Element = ElementA<*>
// or fun exampleUse(elements: List<ElementA<*>>)
// if you don't create Element
fun exampleUse(elements: List<Element>) {
fun <T> use(elementA: ElementA<T>) {
// We don't care about the actual type T, just that it exists
elementA.doSomething(elementA.produce())
}
for (element in elements) {
use(element)
}
}
Note that the local function can also be an extension function:
fun exampleUse(elements: List<Element>) {
fun <T> ElementA<T>.use() {
// We don't care about the actual type T, just that it exists
doSomething(produce())
}
for (element in elements) {
element.use()
}
}
I'm not sure if this meets your requirements, but I implemented a type-safe heterogenous map in Kotlin: https://github.com/broo2s/typedmap . It is a little different than your example, because it is not a list, but a map. You can use it for example like this:
val map = simpleTypedMap()
sess += User("alice")
val user = sess.get<User>()
You can still store multiple items per a key type, but as this is a map, each item has to be uniquely identifiable, so I'm not sure if this suits you:
map[UserKey(1)] = User("alice")
map[UserKey(2)] = User("bob")
val alice = map[UserKey(1)]
val bob = map[UserKey(2)]
class Family() {
fun addMember(name: String) {}
inline operator fun invoke(body: Family.() -> Unit) {
body()
}
}
After using invoke operator function I can do something like this:
val family = Family().invoke { this:
addMember("Android")
}
I have seen code structures where the declaration is simply by doing this.
val family = Family { this:
addMember("Android")
}
In current situation invoke can be called on a instance i.e.
val family = Family()
family {
addMember(...)
}
If I understood correctly you want to create a new instance of Family and apply the block into the object, you can do this by putting the function under a companion.
class Family {
fun addMember(name: String) {}
companion object {
inline operator fun invoke(body: Family.() -> Unit): Family {
return Family().apply(body)
}
}
}
Now:
val family = Family {
addMember(...)
}
Suppose I have two methods:
private fun method1(a: A): A {
return a.copy(v1 = null)
}
private fun method2(a: A): A {
return a.copy(v2 = null)
}
Can I write something like:
private fun commonMethod(a: A, variableToChange: String): A {
return a.copy($variableToChange = null)
}
Another words, can I use a variable to refer to a named argument?
If I understand correctly what you are trying to archive I would recommend to pass a setter to the method e.g.
fun <A> changer (a: A, setter: (a: A) -> Unit ) {
// do stuff
setter(a)
}
Is this what you are looking for?
A possible solution for this problem (with usage of reflection) is:
inline fun <reified T : Any> copyValues(a: T, values: Map<String, Any?>): T {
val function = a::class.functions.first { it.name == "copy" }
val parameters = function.parameters
return function.callBy(
values.map { (parameterName, value) ->
parameters.first { it.name == parameterName } to value
}.toMap() + (parameters.first() to a)
) as T
}
This works with all data classes and all classes that have a custom copy function with the same semantics (as long as the parameter names are not erased while compiling). In the first step the function reference of the copy method is searched (KFunction<*>). This object has two importent properties. The parameters property and the callBy function.
With the callBy function you can execute all function references with a map for the parameters. This map must contain a reference to the receiver object.
The parameters propery contains a collection of KProperty. They are needed as keys for the callBy map. The name can be used to find the right KProperty. If a function as a parameter that is not given in the map it uses the default value if available or throws an exception.
Be aware that this solution requires the full reflection library and therefore only works with Kotlin-JVM. It also ignores typechecking for the parameters and can easily lead to runtime exceptions.
You can use it like:
data class Person (
val name: String,
val age: Int,
val foo: Boolean
)
fun main() {
var p = Person("Bob", 18, false)
println(p)
p = copyValues(p, mapOf(
"name" to "Max",
"age" to 35,
"foo" to true
))
println(p)
}
// Person(name=Name, age=15, foo=false)
// Person(name=Max, age=35, foo=true)
some background:
val (name, age) = person
This syntax is called a destructuring declaration. It creates multiple variables (correction, creates multiple values) at at the same time.
Destructuring declarations also work in for-loops: when you say:
for ((a, b) in collection) { ... }
Lets take a look at a list item i have:
#Parcelize
data class MyModel(
var name: String = "",
var is_locked: Boolean = true,
var is_one_size: Boolean = false,
) : Parcelable
and now i have obtained a list of "MyModel" classes and i am trying to loop over them like this:
private fun initMyModelList(model: MutableList<MyModel>) {
//i want to access is_locked from here with destruction but i cant ? IDE telling me the type is an int but its clearly defined as a Boolean
for((is_locked) in model){
//what i want to do in here is access the is_locked var of the model list and change all of them in a loop. im trying to use Destructuring in loop as a conveience. why is it not working ?
//how can i make the call signature look like this--- > is_locked = true instad of model.is_locked =true
}
}
all i want to do is be able to call is_locked = true instead of model.is_locked = true within the loop. how can this be done ?
This syntax is called a destructuring declaration. It creates multiple variables at at the same time.
It doesn't create multiple variables, it captures multiple values. You're working with values, not references, as your source tells further:
A destructuring declaration is compiled down to the following code:
val name = person.component1()
val age = person.component2()
Closest to what you want would be this custom extension function:
inline fun <E> Iterable<E>.withEach(block: E.() -> Unit) {
forEach {
it.block()
}
}
Use like so:
model.withEach {
is_locked = true
}
Before you ask the obligatory question "why isn't this included in stdlib?" consider that functional style programming typically is about transforming immutable types. Basically, what I did here was encourage a bad habit.
Basically, it isn't possible, cause your code is compiled to something like:
for (m in models) {
val is_locked = m.component1()
...
}
Which means that you create a local property which cannot be reassigned. But you can do something like this:
for (m in model) {
with(m) {
is_locked = true
}
}
Yep, it isn't perfect, but it can be improved with extension methods:
fun <T> List<T>.forEachApply(block: T.() -> Unit) {
forEach(block)
}
private fun initMyModelList(model: MutableList<MyModel>) {
model.forEachApply {
is_locked = true
}
}
You can use destructuring in a loop just fine as read-only values.
data class Stuff(val name: String, val other: String)
fun doStuff() {
val stuff = Stuff("happy", "day")
val stuffs = listOf(stuff)
for ((name) in stuffs) {
println(name)
}
}
Running that method prints "happy" to the console. Baeldung shows an example of using it here.
It's best practice for data classes to be immutable, so I would try to rewrite your data class to be immutable. The .copy function will let you copy your data class but with new, different values.