Hi, I'm learning Contravariance in kotlin and I as...
# getting-started
x
Hi, I'm learning Contravariance in kotlin and I asked GPT when to use it , but it give me some code that didn't work in kotlin playground. here is the code :
Copy code
// Base class for documents
open class Document {
    open fun printContent() = "Printing generic document content"
}

// PDF document subclass
class PdfDocument : Document() {
    override fun printContent() = "Printing PDF content"
}

// Word document subclass
class WordDocument : Document() {
    override fun printContent() = "Printing Word content"
}

// Printer interface, contravariant in T
interface Printer<in T> {
    fun print(document: T)
}

// Implementation of a printer that can print any type of document
class GeneralPrinter : Printer<Document> {
    override fun print(document: Document) {
        println(document.printContent())
    }
}

// Implementation of a printer that is specialized for PDF documents
class PdfPrinter : Printer<PdfDocument> {
    override fun print(document: PdfDocument) {
        println("PDF Printer: ${document.printContent()}")
    }
}

fun main() {
    val pdfDocument = PdfDocument()
    val wordDocument = WordDocument()

    val generalPrinter: Printer<Document> = GeneralPrinter()
    val pdfPrinter: Printer<PdfDocument> = PdfPrinter()

    // PdfPrinter can print PdfDocument
    pdfPrinter.print(pdfDocument)

    // GeneralPrinter can print any document
    generalPrinter.print(pdfDocument)
    generalPrinter.print(wordDocument)

    // Thanks to contravariance, we can assign a PdfPrinter to a Printer<Document>
    val printer: Printer<Document> = PdfPrinter() // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! error here in kotlin playground
    printer.print(pdfDocument) // Valid, PdfDocument is a Document

    // This line would result in a compile-time error if uncommented
    // printer.print(wordDocument) // Invalid, WordDocument is not a PdfDocument
}
the error is "Type mismatch: inferred type is PdfPrinter but Printer<Document> was expected" , even if the code works I still don't know why not define the interface like this:
Copy code
// without using the keyword in 
interface Printer<T> {
    fun print(document: T)
}
so, could anyone tell me what is the best practice to use the keyword
in
which means Contravariance ( please compare with not using
in
might have what kind of risks )
s
good explaination about the ins and outs of variance in Kotlin: https://kt.academy/article/ak-variance
🙌 1
c
In general, ChatGPT doesn’t actually understand code, so it’s probably not going to be very helpful for actually understanding anything, and as you noted, it usually yields invalid or incorrect code snippets. To the specific question, though, the error seems valid to me, though I’m far from an expert on variance. But the base
Printer
class uses
in
to declare that it can take in a value of any kind of
Document
, but when you extend it with
PdfPrinter
, that no longer holds true. The
PdfPrinter
subclass now restricts the types that it is able to take in to only
PdfDocument
. You cannot assign
PdfPrinter
to
Printer<Document>
because you cannot pass any kind of document to it. You can only pass in a
PdfDocument
. So it would not make sense to be able to assign
PdfPrinter
to
Printer<Document>
, because that would allow you to try and pass a
WordDocument
in, for example, which would then throw a
ClassCastException
because the
WordDocument
is not a
PdfDocument
. To extend the example in a way that might help you understand, let’s look at an example using
out
variance.
Copy code
interface Loader<out T: Document> {
    fun load(fileName: String): T
}
class PdfLoader : Loader<PdfDocument> {
    override fun load(fileName: String): PdfDocument = TODO()
}
class WordDocumentLoader : Loader<WordDocument> {
    override fun load(fileName: String): WordDocument = TODO()
}
Here, you are able to assign
PdfLoader
to
Loader<Document>
, since
load()
returns an instance of a
PdfDocument
. You can safely assign
PdfDocument
to a variable of type
Document
, and therefore there’s nothing unsafe with treating a
Loader<PdfDocument>
as just
Loader<Document>
. It’s not that the Loader suddenly gains the ability to load any type of document (it still only returns instances of
PdfDocument
), but you are declaring your intent to be that you do not care about the subtype, and are only concerned with the base
Document
.
Copy code
val loader: Loader<Document> = PdfLoader()
val pdfDocument: Document = loader.load("document1.pdf")
val generalDocument: PdfDocument = loader.load("document1.pdf") // error, loader is only aware that it returns a `Document`
And to bring the two together, removing the variance bounds of the generic type parameter basically just treats it as being both
in
and
out
simultaneously. For example, you can create an interface that extends both
Printer
and
Loader
with the same type variable as long as it doesn’t have variance
Copy code
// does not compile is T is `in` or `out`
interface DocumentContext<T: Document> : Loader<T>, Printer<T> 

public class PdfHelper : DocumentHelper<PdfDocument> {
    override fun print(document: PdfDocument) { TODO() }
    override fun load(fileName: String): PdfDocument  = TODO()
}

val context: DocumentHelper<PdfDocument> = PdfHelper()
val loader: Loader<Document> = context
val pdfLoader: Loader<PdfDocument> = context
val printer: Printer<Document> = context // error, cannot pass any generic Document. Limited to PdfDocumentes
val pdfPrinter: Printer<PdfDocument> = context
👏🏻 1
x
Great Post! but I have a small question. I didn’t notice any obvious advantage in using the keyword 'in'. say we can change the code from
Copy code
interface Sender<in T : Message> {
    fun send(message: T)
}

class GeneralSender(serviceUrl: String) : Sender<Message> {
    private val connection = Connection("")

    override fun send(message: Message) {
        connection.send(message)
    }
}

fun main() {
    val orderManagerSender: Sender<OrderManagerMessage> = GeneralSender("orderManagerURL")
    orderManagerSender.send(OrderManagerMessageImpl())
}
to this
Copy code
interface Sender<T : Message> {
    fun send(message: T)
}

class GeneralSender<T : Message>(serviceUrl: String) : Sender<T> {
    private val connection = Connection("")

    override fun send(message: T) {
        connection.send(message)
    }
}

fun main() {
    val orderManagerSender: Sender<OrderManagerMessage> = GeneralSender("orderManagerURL")
    orderManagerSender.send(OrderManagerMessageImpl())

    val invoiceManagerSender: Sender<InvoiceManagerMessage> = GeneralSender("invoiceManagerURL")
    invoiceManagerSender.send(InvoiceManagerMessageImpl())
}
it seems we can do the same thing without using
in
@Stephan Schröder
s
This will be a bit inprecise, but 'in' and 'out' are basically the declaration site alternative to generic T super SomeType and T extends SomeType as long as you only end up putting the class T itself into your class, it doesn't make a difference. "in" means you can only use the generic class T as the class of parameter in you class, but not as the class of the returnvalue. Let's assume a class hirachy of three classes: LivingThing -> Animal -> Dog each you can assign val source: SourceAnimal = sourceOfDogs (with the type of sourceOfDogs: SourceDogl) since each dog is also a instance of Animal also you can assign val sink: SinkAnimal = sinkOfLivingThings (with the type of sinkOfLivingThings: SinkLivingThing) since whatever can take care of living things, can also take care of animals in order for the compiler to be ok with this, you need to declare something like
Copy code
interface Source<out T>
and
Copy code
interface Sink<in T>
You might want to reread the documentation about generics 😅