Feedback for my Pattern-Matching lib

I’m writing a small library for “Scala-like” pattern-matching in when-expressions. It’s not ready yet (especially collections need some more love), and I’m looking for feedback and criticism. Here is how it can look like:

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

val p = Person("Alice", "Cooper", 74)

when(match(p)) {
    "Alien"(any, any, any) -> println("Aliens!")
    "Person"("Mick", "Jagger", gt(70)) -> println("Mick Jagger!")
    "Person"("Alice", "Cooper", any) -> println("Alice Cooper!")
    else -> println("I don't know this guy")
}
1 Like

Looks interesting as a something that hacks through existing language.

One question: why "Person"(...) and not Person::class(...)? Would it be technically impossible? Do you consider it worse than a string?

Also, polluting global space with String.invoke() is pretty aggressive. I think it would be better to provide this operator through DSL. It could be hard to merge this with when though. Maybe something like:

p.match { when (it) {
   ...
}}

Not ideal, I agree.

edit: Or resign from using when entirely and do something like:

p.match {
    "Alien"(...) then { println() }
    "Person"(...) then { println() }
    ...
}

But then I think {} would be mandatory.

Thank you for the feedback!

I switched from String.invoke to KClass.invoke. The String version is a little shorter, but I think using a KClass feels a little more precise and safe (and actually simplifies the code). One thing I didn’t think of before is that renaming a class would spell disaster in the String version. Also, having invoke on the lesser used KClass feels not as unsafe as for String.

I thought about a seperate DSL imitating when, but I don’t see a big advantage, and the user has the burden of learning a new syntax, even if it looks similar. The only thing that might be prettier in an own DSL is the capture pattern, and I don’t think it’s worth it.

2 Likes

For the indexed access, I also tried to use reflection classes instead of String, and it works great. I think I’ll keep both:

when(match(p)) {
  startsWith("B")["firstName"] ->... // old String based access
  startsWith("C")[Person::firstName] -> ... // more type safe
}

If you can wrap your head around that you have to go reverse, you can even have “nested” access:
eg(5)[String::length][Person::firstName]

1 Like

Maybe this could be better:

eg(5) { person: Person -> person.firstName.length }
// or just simply
eg(5) { it.firstName.length }

I thought about something like this, but at this place I don’t know that I need to start from Person, so I could do only something like (Any) -> Any or need type annotations, which is not great. Further, it would probably mean to restructure large parts of the DSL, while [] is totally orthogonal to other features by using an extension method with receiver.

I’ll ponder over this, maybe there is another syntax to make it work, as it would be useful to have.

1 Like

Yes, I thing I have something I could live with. Using reified generics, this would work

when(match(person)) {
        eq(5) on { p:Person -> p.firstName.length } -> ...
        eq(5).on<Person>{ it.firstName.length } -> ...
        eq("Alice") on Person::firstName -> ...
        else -> ...
}

The second version can’t use infix, but I think all cases look okay-ish.

1 Like

I added a second style to the library using a similar syntax to @broot’s suggestion, it looks like this:

val result = match(p) {
    Person::class("Alice", "Smith", gt(3)) then { "It's Alice!" }
    otherwise { "someone else" }
}