I am struggling with types. I have a very minimal...
# getting-started
p
I am struggling with types. I have a very minimal TYPED rule definition. I'd like to have a RuleSet (with heterogeneous rules). In fire I'd like to invoke (and return) those rules, which are of the correct types, ideally in a typesafe way. So how to make the
fire
method typesafe? I mean, how can I filter out those Rules, where
clazz
is incompatible with the
arg
?
j
I think your main problem here is that you're trying to call
Rule<*>.fire()
with an incompatible argument. If you declare this function to only take
T
, it cannot take other types. You might instead want to only call rules that do support your type, using the
clazz
property to make this check.
p
Ok, fine, can I prefilter all rules (and narrow the type of them to T?)
Does the clazz variable help somehow, or can I remove from the Rule type?
t
Unfortunately, classes can't have reified types. That makes the following a bit redundant but the compiler is happy:
Copy code
class RuleSet(val rules: MutableList<Rule<*>>) {
    inline fun <reified T : Any> fire(arg: T): Set<Rule<T>> {
        return rules
            .filter { it.clazz == T::class }
            .filterIsInstance<Rule<T>>() // logically redundant but compiler-friendly
            .filter { it.fire(arg) }
            .toSet()
    }
}
Because we need
T
to be
reified
, the function needs to be
inline
, thus
rules
needs to be public. Tested with class
RuleSet
and standalone function:
Copy code
import kotlin.reflect.KClass

fun main() {
    val rules = listOf(
        Rule(Int::class, "Int1", { this > 0 }, { println("fired $name") }),
        Rule(Int::class, "Int2", { this > 0 }, { println("fired $name") }),
        Rule(String::class, "String1", { this != "" }, { println("fired $name") }),
        Rule(Boolean::class, "Boolean", { this }, { println("fired $name") }),
    )

    println(rules.fire(1))

    val ruleSet = RuleSet(rules.toMutableList())
    println(ruleSet.fire(1))
}

class Rule<T : Any>(
    val clazz: KClass<T>,
    val name: String,
    private val whenTrue: T.() -> Boolean,
    private val thenDo: Rule<T>.(T) -> Unit
) {
    fun fire(arg: T): Boolean {
        if (whenTrue(arg)) {
            thenDo(arg)
            return true
        }
        return false
    }
}

class RuleSet(val rules: MutableList<Rule<*>>) {
    inline fun <reified T : Any> fire(arg: T): Set<Rule<T>> {
        return rules
            .filter { it.clazz == T::class }
            .filterIsInstance<Rule<T>>()
            .filter { it.fire(arg) }
            .toSet()
    }
}

inline fun <reified T : Any> List<Rule<*>>.fire(arg: T): Set<Rule<T>> {
    return this
        .filter { it.clazz == T::class }
        .filterIsInstance<Rule<T>>()
        .filter { it.fire(arg) }
        .toSet()
}
😮 1
🤔 1
j
I was thinking about this:
Copy code
class RuleSet(private val rules: MutableList<Rule<*>>) {
    fun <T : Any> fire(arg: T): Set<Rule<in T>> = rules
        .filter { it.clazz.isInstance(arg) }
        .map {
            @Suppress("UNCHECKED_CAST") // we checked it in the filter above
            it as Rule<in T>
        }
        .filter { it.fire(arg) }
        .toSet()
}
Which does require the
clazz
property still. It doesn't need
T
to be reified, nor
fire
to become
inline
. We are using reflection anyway for the instance check
@Tobias Suchalla your solution is interesting but the
==
check is a bit restrictive. It means we won't fire rules that can handle a supertype of the value
p
Seems to work without inline
j
Yeah using reflection (with
it.clazz.isInstance
) means you don't need the reification for the actual check. You would need it for
filterIsInstance
if it were actually testing something with
T
, but it doesn't check the type parameter here so it only works with
Rule
as a raw type, hence why you can get away without reifying
T
(we're kinda exploiting a loophole here). This is why I prefer using the
map
approach with explicit cast and warning suppression to show it's just a compiler trick.
today i learned 1
Also, this last snippet is wrong @Pihentagy: if you check
it.clazz.isInstance(arg)
then you cannot cast your rules to
Rule<T>
(again, don't trust
filterIsInstance
, you're being fooled and the compiler too). It should be
Rule<in T>
, because the rule may in fact be defined for supertypes of the current
T
, so their
clazz
property will not be
T
.
p
Oh, because of type erasure again?
But it does seems to work
Is there a kotlin playground, where I can paste?
j
This is more about a semantic problem: when you're checking with
==
(as in Tobias's code), you're effectively checking that
T
is the type of the rule, so it's a
Rule<T>
. When you're checking with
clazz.isInstance(arg)
, you're checking that the type
T
is acceptable by the rule, and is a subtype of the rule's parameter. So you're only checking that it's a
Rule<in T>
.
Is there a kotlin playground, where I can paste?
https://play.kotlinlang.org/
👍 1
> But it does seems to work The behavior of the method will not be affected by this issue. Only the return type will. This means that if you try to use the returned rules, you might run into issues. For instance:
Copy code
val appliedRules: Set<Rule<Int>> = ruleSet.fire(42) // incorrect
Here it's wrong to say that
appliedRules
only contains rules of type
Rule<Int>
. It may contain
Rule<Comparable<Int>
. If you try to access
appliedRules.first().clazz
it will give you a wrong class type.
p
Oh, I see. In that case using parametrized type with filterIsInstance should be an error (or at least a warning), right?
j
Yeah, I believe it would be better if it did warn in this case, the same way an unchecked cast generates a warning.