Sunday, 26 July 2020

Generics – Out, In Using Kotlin

Generics feature is template kind of implementation approach, which allow us to define classes, methods and properties which are accessible using different data types and keep a check of the compile-time type safety.


A generic type is a class or function that is parameterized over types. We always use angle brackets (<>) to specify the type parameter in the program.


Pros:

Compile time safety: Generics code is checked at compile time for the parameterized type so that it avoids run time error


Type casting: No need to typecast the object.


Type safety: Generic allows only single type of object at a time.


If you ever defined generic in Kotlin, you’ll notice many a times, it would propose to use the in or out keyword to define the generic. It puzzles me at a start on when which is used, and for what.

Formally, this is a way to define Covariant & Contravariance. Let’s see with the example in simple Word Processing and its implementation on Consumer (User) & Producer (Output Mode).


Out (Covariant):  If your generic class/interface only use the generic type as output of its function/s, it won't allow us to add the setter function with T


interface ProduceOutput<out T> {
    fun getContent(): T
}

This ProduceOutput interface, as it is mainly to produce output of the generic type. 
Side Note: produce = output = out

In (Contravariance): If your generic class/interface only use the generic type as input of its function/s, it won't allow us to getter function with T

interface ConsumeInput<in T> {
    fun consumeContent(data: T)
}

This ConsumeInput interface, as it is mainly to consuming the generic type. 
Side Note: consume = input = in

Before we see how the implementation on Producer and Consumer, lets create a necessary class hierarchy, which is going to handle the publishing industry on their document processing with their consumer base.


open class WordProcessor(val content: String, val isStyle: Boolean = false, val isAnimated: Boolean = false) {
    fun show() {
        println("Type : $content | Style: $isStyle | Animation: $isAnimated")
    }

    fun showConsumer() {
        println("Input : $content | Style: $isStyle | Animation: $isAnimated")
    }
}

open class Notes(input: String) : WordProcessor(input)
class Transcript(input: String) : Notes(input)
class FormattedNote(input: String) : WordProcessor(input, true)
class DigitalContent(input: String) : WordProcessor(input, true, true)

Implementation classes for producing the output via software tools as below

class GeneralProcessing : ProduceOutput<WordProcessor> {
    override fun getContent(): WordProcessor {
        return WordProcessor("Content Processor General")
    }
}

class Notepad : ProduceOutput<Notes> {
    override fun getContent(): Notes {
        return Notes("Notepad Content")
    }
}

class MSWord : ProduceOutput<FormattedNote> {
    override fun getContent(): FormattedNote {
        return FormattedNote("Formatted Content")
    }
}

class Flash : ProduceOutput<DigitalContent> {
    override fun getContent(): DigitalContent {
        return DigitalContent("Graphic Content")
    }
}

class SubEditPlayer : ProduceOutput<Transcript> {
    override fun getContent(): Transcript {
        return Transcript("Subtitle Content")
    }
}

For 'out' generic, we could assign a class of subtype to class of super-type, Please check out the comments in the code.

/*out -- This subtype class will be assigned only to the same type or super type class*/
    
val meetingMinutes: ProduceOutput<WordProcessor> = Notepad()
val wallPoster: ProduceOutput<WordProcessor> = MSWord()
val flashGame: ProduceOutput<WordProcessor> = Flash()
val osDefaultDoc: ProduceOutput<WordProcessor> = GeneralProcessing()
val youtubeSubtitle: ProduceOutput<Transcript> = SubEditPlayer()

// Eg: out compiler error
// val meetingRecording:ProduceOutput<DigitalContent> = GeneralProcessing() // Error
// val poster:ProduceOutput<Notepad> = GeneralProcessing() //Error
val twoDGame: ProduceOutput<DigitalContent> = Flash() // This works - Same Class Type
val subTile: ProduceOutput<WordProcessor> = SubEditPlayer() // This works --  subtype assigned to supertype

meetingMinutes.getContent().show()
wallPoster.getContent().show()
flashGame.getContent().show()
osDefaultDoc.getContent().show()
youtubeSubtitle.getContent().show()
twoDGame.getContent().show()
subTile.getContent().show()

Implementation classes of the users who consumes i.e. prepare or use these tools

class GeneralReviewer : ConsumeInput<WordProcessor> {
    override fun consumeContent(data: WordProcessor) {
        println("GeneralReviewer -- Review the content")
        data.showConsumer()
    }
}

class Reporter : ConsumeInput<Notes> {
    override fun consumeContent(data: Notes) {
        println("Reporter - Taken Rough Notes")
        data.showConsumer()
    }
}

class ContentDesigner : ConsumeInput<FormattedNote> {
    override fun consumeContent(data: FormattedNote) {
        println("Designer - Formatted the content")
        data.showConsumer()
    }
}

class GraphicDesigner : ConsumeInput<DigitalContent> {
    override fun consumeContent(data: DigitalContent) {
        println("Graphic Designer -- Added Animation Effects to the content")
        data.showConsumer()
    }
}

class SubtitleWriter : ConsumeInput<Transcript> {
    override fun consumeContent(data: Transcript) {
        println("Subtitle Writer -- Updated subtitle content")
        data.showConsumer()
    }
}

For ‘in' generic, we could assign a class of super-type to class of subtype, Please check out the comments in the code.

/*in -- This supertype class will be assigned only to the same type or subtype class*/

val takeNotes: ConsumeInput<Notes> = Reporter()
val pageMaker: ConsumeInput<FormattedNote> = ContentDesigner()
val adFilm: ConsumeInput<DigitalContent> = GraphicDesigner()
val generalContent: ConsumeInput<WordProcessor> = GeneralReviewer()
val subTag: ConsumeInput<Transcript> = SubtitleWriter()

// Eg: in compiler error
// val takeNotes2: ConsumeInput<WordProcessor> = Reporter() // Error
// val pageMaker2: ConsumeInput<WordProcessor> = ContentDesigner() // Error
// val adFilm2: ConsumeInput<WordProcessor> = GraphicDesigner() // Error
// val tagline: ConsumeInput<WordProcessor> = SubtitleWriter() // Error
val generalContent2: ConsumeInput<WordProcessor> = GeneralReviewer() // No Error Same Type
val subTag2: ConsumeInput<Transcript> = GeneralReviewer() // No Error -- supertype assigned to subtype

takeNotes.consumeContent(Notes("Hello, Welcome to meeting"))
pageMaker.consumeContent(FormattedNote("Thank you all"))
adFilm.consumeContent(DigitalContent("BGM Rocks"))
generalContent.consumeContent(WordProcessor("Great to review"))
generalContent2.consumeContent(WordProcessor("GeneralContent2: Great to review"))
subTag.consumeContent(Transcript("Display at the bottom"))
subTag2.consumeContent(Transcript("Subtag 2: Display at the bottom"))

Very simple way to remember In and Out to be used on the implementation.  Also, importantly if anything we do mistakes, it will stop us at the compile time itself.  

Use IN => Super Type could be assigned subtype
Use OUT => Sub Type could be assigned to Super Type


No comments: