https://kotlinlang.org logo
#getting-started
Title
# getting-started
n

Norbi

11/24/2023, 11:12 AM
What is the correct way to resolve the following generics-related compilation error?
Copy code
interface Formatter<in T> {

    fun supports(value: Any?): Boolean

    fun format(value: T): String
}

class IntFormatter : Formatter<Int> {

    override fun supports(value: Any?): Boolean = value is Int

    override fun format(value: Int): String = value.toString()
}

class DoubleFormatter : Formatter<Double> {

    override fun supports(value: Any?): Boolean = value is Double

    override fun format(value: Double): String = value.toString()
}

interface GenericFormatter {

    fun format(value: Any?): String
}

class GenericFormatterImpl(
    private val formatters: List<Formatter<*>>
): GenericFormatter {

    override fun format(value: Any?): String =
        formatters
            .firstOrNull { it.supports(value) }
            ?.format(value) // Compilation error: Type mismatch. Required: Nothing Found: Any?
            ?: value.toString()
}

val genericFormatter: GenericFormatter = GenericFormatterImpl(listOf(IntFormatter(), DoubleFormatter()))
Thanks.
e

ephemient

11/24/2023, 11:36 AM
you don't show
formatters
but it likely has type
List<Formatter<Nothing>>
, so you can't do anything without some unchecked casting, e.g.
as Formatter<Any?>?
before invoking
?.format(value)
a

aishwaryabhishek3

11/24/2023, 11:45 AM
You will have to use unchecked casting
Copy code
fun format(value: Any?): String =
    formatters
        .firstOrNull { it.supports(value) }?.let {
            (it as? Formatter<Any?>)?.format(value)
        } ?: value.toString()
🙏 1
1
n

Norbi

11/24/2023, 11:59 AM
@ephemient I have added the missing code, thanks.
e

ephemient

11/24/2023, 12:11 PM
in *
is effectively
Nothing
, ditto
out *
and
Any?
as per the docs
because nothing can be guaranteed to be a "subtype of *"
n

Norbi

11/24/2023, 12:25 PM
If I change
GenericFormatterImpl
to
Copy code
class GenericFormatterImpl(
    private val formatters: List<Formatter<Any?>>
): GenericFormatter {
then it compiles. But in this case the instantiation does not compile:
Copy code
val genericFormatter: GenericFormatter = GenericFormatterImpl(listOf(IntFormatter(), DoubleFormatter())) // Type mismatch. Required: List<Formatter<Any?>> Found: List<Formatter<*>>
j

Joffrey

11/24/2023, 1:10 PM
Because that would be lying. A
Formatter<Int>
is NOT a
Formatter<Any?>
because it cannot format other things than ints (e.g. it cannot format a
Double
). A
Formatter<Any?>
is expected to be able to take in any value of type
Any?
(which is basically anything). This constraint comes from the position of the
T
as argument to the
format()
method, and it is formalized by using
in T
in the
Formatter
declaration. Both of those make sense, and rightfully forbid you from putting a `Formatter<Int>`in a
List<Formatter<Any?>>
🙏 1
n

Norbi

11/24/2023, 1:39 PM
Thanks for the quick replies. The final solution seems to be (the other parts are unchanged):
Copy code
class GenericFormatterImpl(
    private val formatters: List<Formatter<*>>
) : GenericFormatter {

    override fun format(value: Any?): String =
        (formatters
            .firstOrNull { it.supports(value) }
                as? Formatter<Any?> // Explicit cast
                )
            ?.format(value)
            ?: value.toString()
}
j

Joffrey

11/24/2023, 1:41 PM
Yes, as others have mentioned, your problem here is not really in the declaration of the
formatters
property itself. It's rather that you have information that the compiler doesn't know: the fact that
formatter.supports(value) == true
means that the type of
value
is a subtype of
T
for this
formatter
. Since the compiler cannot deduce this on its own (at least in the current design), you have to cast
🙏 2
today i learned 2
c

CLOVIS

11/24/2023, 1:44 PM
I don't think that cast is safe. Generics are erased, so
as Formatter<WhateverYouWant>
only checks that the class inherits from
Formatter
, it cannot check whether the generic type is correct or not. If you had an incorrect implementation of
supports
, I think this crashes at runtime on the
format
call, since the
as
will always succeed
j

Joffrey

11/24/2023, 1:47 PM
Yes that's the risk. This cast is merely an escape hatch to circumvent the compiler error because "we know better" based on the contract of
supports()
. Of course if it turns out we're wrong (someone broke this contract), then telling the compiler "I know better" will lead to incorrect behaviour or runtime crashes.
👍 1
👍🏻 1
d

Daniel Pitts

11/24/2023, 7:42 PM
Copy code
interface Formatter {
    fun tryFormat(value: Any?): String?
}

class IntFormatter : Formatter {
    override fun tryFormat(value: Any?): String? = (value as? Int)?.let(Int::toString)
}

class DoubleFormatter : Formatter {
    override fun tryFormat(value: Any?): String? = (value as? Int)?.let(Int::toString)
}

class GenericFormatter(private val formatters: List<Formatter>):Formatter {

    override fun tryFormat(value: Any?): String? =
        formatters.asSequence().map { it.tryFormat(value) }.firstOrNull()
}
Having separate "supports" vs "format" is what causes the issue. Combining them as
tryFormat
will provide a cleaner impl