Decomposing data classes

I’m working on a little pattern matching library called kopama. So far, I use KSP to allow the decomposition of data classes, e.g. for this code

@Kopama
data class Person(val firstName: String, val lastName: String, val age: Int)

there would be a function generated that takes sub-patterns for the fields and returns a pattern for the whole class (a pattern is just a “test function” (T) -> Bool) :

fun person(
  firstName: Pattern<String> = any,
  lastName: Pattern<String> = any,
  age: Pattern<Int> = any
): Pattern<Person?> = {
  when(it) {
    null -> false
    else -> firstName(it.firstName) &&
            lastName(it.lastName) &&
            age(it.age)
  }
}

This can be now used in expressions like person(startsWith("Dan"), any, gt(18)), where the arguments are the mentioned sub-patterns

Now I found a way to do it without KSP, but via interfaces, and I would like to get your input before doing such a drastic change. First some preparations:

data class T3<A, B, C>(val a: A, val b: B, val c: C)

private fun Any.getComp(n: Int): Any? =
    this::class.members.filter { 
        it.name == "component$n" && it.parameters.size == 1 
    }.first().call(this)

interface Unapply3<A, B, C> {
    fun unapply(): T3<A, B, C> = T3(getComp(1), getComp(2), getComp(3)) as T3<A, B, C>
}

operator fun <A, B, C, U : Unapply3<A, B, C>> KClass<U>.invoke(
    pa: Pattern<A>,
    pb: Pattern<B>,
    pc: Pattern<C>
): Pattern<Unapply3<A, B, C>> = {
    val (a, b, c) = it.unapply()
    it.javaClass.simpleName == this.simpleName && pa(a) && pb(b) && pc(c)
}

The equivalent example code would look now like this:

data class Person(
    val firstName: String, 
    val lastName: String, 
    val age: Int
): Unapply3<String, String, Int>

The example pattern would be now Person::class(startsWith("Dan"), any, gt(18)).

Getting rid of KSP would be a very nice improvement, but the need to specify the generics is inconvenient, and it isn’t even type save, at least I found no way to enforce that the type parameters of the respective Unapply interface match the types of the constructor args.

What do you think of this idea, and which version (KSP annotation vs interface) would you prefer?

3 Likes

I vote for your current approach. It would be nice to get rid of KSP and use only interfaces, but your current approach is much more readable.

1 Like

Yes, I agree, and there is another problem I cannot solve with interfaces: I can’t get named arguments working. I think that qualifies as “close, but not cigar”, so I’ll keep the KSP version…

I think that’s a good call. I checked out your repo. Kopama looks interesting. We’ll give it a spin when you have a production release.

This is really interesting to me. My need is to destructure a potentially complicated DAG and capture values that match tests, returning a tuple. So the branching logic application you demonstrate isn’t really applicable (I don’t need then) but it might be pretty close if I fork it…

1 Like