Welcome back to CodeYourCraft! Today, we're diving into the fascinating world of Kotlin and learning about Contravariance (in). This concept is an essential part of type theory that will help you write more flexible and efficient code. Let's get started! 🎯
Contravariance is a principle in Kotlin that allows us to use supertype references where subtype objects are passed as arguments. In simpler terms, it means we can pass a subtype where a supertype is expected in function arguments. 💡 Pro Tip: Contravariance is denoted by the in keyword in Kotlin.
Let's illustrate this with a practical example. Suppose we have a Printable interface and two classes Book and Newspaper that implement it. Now, we have a printAll function that takes an array of Printable objects.
interface Printable {
fun print()
}
class Book(val title: String) : Printable {
override fun print() {
println("Printing Book: $title")
}
}
class Newspaper(val headline: String) : Printable {
override fun print() {
println("Printing Newspaper: $headline")
}
}
fun printAll(printables: Array<Printable>) {
for (printable in printables) {
printable.print()
}
}Now, let's say we want to create a new function printBooksAndNewspapers that takes an array of both Book and Newspaper objects. Since our printAll function accepts an array of Printable, we can use it to achieve this.
fun printBooksAndNewspapers(booksAndNewspapers: Array<out Printable>) {
printAll(booksAndNewapapers)
}In the above code, we used the out keyword to indicate that the Array<out Printable> can contain subtypes of Printable. Now, we can call the printBooksAndNewspapers function with an array containing both Book and Newspaper objects.
fun main() {
val books = arrayOf(Book("Learning Kotlin"), Book("Advanced Kotlin"))
val newspapers = arrayOf(Newspaper("Tech News"), Newspaper("Sports News"))
printBooksAndNewspapers(books + newspapers)
}Contravariance can also be applied to function types. In this case, we can use a subtype where a supertype is expected as the return type of a function.
interface Printer {
fun print(): String
}
class BookPrinter(val printer: Printer) : Printer {
override fun print(): String {
val bookPrintOutput = printer.print() + " (Book)"
return bookPrintOutput
}
}
class NewspaperPrinter(val printer: Printer) : Printer {
override fun print(): String {
val newspaperPrintOutput = printer.print() + " (Newspaper)"
return newspaperPrintOutput
}
}
fun printPrinter(printer: Printer): String {
return printer.print()
}Now, we can use our BookPrinter and NewspaperPrinter classes to create new printers that extend the base Printer class. Since our printPrinter function returns a Printer, we can use it to create a function printBookAndNewspaper that takes a function returning a Printer.
fun printBookAndNewspaper(printFunction: (Printer) -> Printer): String {
val bookPrinter = BookPrinter(printFunction(NewspaperPrinter(PrintingText("Default Printer"))))
val newspaperPrinter = NewspaperPrinter(printFunction(BookPrinter(printFunction(NewspaperPrinter(PrintingText("Default Printer"))))))
return printPrinter(bookPrinter) + "\n" + printPrinter(newspaperPrinter)
}
fun main() {
val result = printBookAndNewspaper { it }
println(result)
}In the above example, we used the (Printer) -> Printer function type to indicate that printBookAndNewspaper expects a function taking a Printer and returning a Printer.
What does the `out` keyword indicate in Kotlin?
That's all for today! Contravariance (in) is a powerful concept that will help you write more flexible and reusable code in Kotlin. In the next lesson, we'll explore the concept of Covariance (out) and how it differs from Contravariance. Stay tuned! 📝 Note: If you have any questions or need clarification, feel free to ask in the comments section below. Happy coding! ✅