The Programmers Guide To Kotlin - Covariance & Contravariance
Monday, 21 January 2019
Article Index
The Programmers Guide To Kotlin - Covariance & Contravariance
Generic Variance
Type Projections

kotlinlogo

Type Projections

Type projections solve the problem of changing the behavior of a generic class after it has been declared. It also allows you to take a generic entity and while it might not use the type parameter as a pure input or output you can promise to use it as strictly input or output.

For example, consider the array. This is by default invariant and you cannot sensibly redefine its declaration to make it contravariant or covariant because it makes little sense to have array elements that are exclusively read- or write-only. Instead you can use in and out in type modifiers to mark types that will only act as consumers or producers and hence define the type’s variance.

For example:

var a:Array<Any>
var b = Array<Int>(10,{0})
a=b

produces a compile-time error because Array is invariant.

You can't change the fact that arrays are invariant, but you can define the type of a so that it is a consumer:

var a:Array<out Any>
var b = Array<Int>(10,{0})
a=b

The out modifier says that a will only be used to access array elements and this is safe because the underlying elements are ints which, as a derived type of Any, have all the methods and properties required.

What is important here is to realize that this projection is mostly a trick to allow the upcast to pass the compiler's type checking. The elements of the array referenced by a are still ints. What stops you from assigning to a[i] even though you have promised you won’t? The simple answer is that a[i] is regarded as an Int so assigning, say, a string to it will fail because the types are incompatible.

You can try the same trick with a covariant in:

var a:Array<in Int>
var b = Array<Any>(10,{0})
a=b

without the in this too generates a compile-time error. With the in it compiles and you can store values of type Any in elements of a. The compiler allows the assignment but in this case there is nothing to stop you from using a as a source or a consumer of values. That is:

a[1]="abcd"
println(a[1])

works and if you expect a[1] to be an Int just because a is Array<in Int> you are going to be disappointed. This type projection doesn't work as well as it could.

If you use a projection as a function parameter, then the compiler will also check that you are playing by the rules. For example:

fun myFunc(myparam:MyClass<out Any>){
    println(myparam.read())
    myparam.write(10)
}

In this case the compiler will complain about the use of write which attempts to change the value.

Similarly if you define the function as:

fun myFunc(myparam:MyClass<in Int>){
    println(myparam.read()+1)
    myparam.write(10)
}

then the compiler will complain about the + operator as we cannot guarantee that the value is numeric.

The in and out modifiers used as type projections simply allow the up and downcasting of the type and tell you that it is safe to set or get elements.

The * Projection

Final version in book

Summary 

  • Generics are an attempt at allowing algorithms that work with a range of types to be written in a type safe way.

  • Kotlin generics, like those in Java and most other languages, make use of type parameters indicated by <T>. These are used in generic code as if they were a type specifier and assigned a value when the generic code is used to operate on a particular type.

  • The big problem in using generics is that you cannot assume anything about the type T even though you may know at run time what it is. This means you cannot call any methods or use properties beyond that possessed by Any.

  • Generic properties pose a particular problem because you cannot even initialize them as you don’t know their type at compile time.

  • You can create generic properties but only if you include them in the primary constructor so that they are guaranteed to be initialized at run time.

  • One approach to creating generic methods or functions that can do more than just work with Any is to pass a generic action function which has a defined type at compile time.

  • A second approach is to use type constraints. Kotlin provides the upper bound constraint which specifies the base class that the type must be derived from. This allows you to use the methods and properties of the base class within the generic code.

  • Variance is all about how data structures relate to one another when they are composed of related types. That is, if you construct a new type involving an existing type then it is contravariant if the construction reverses the “use in place of” relationship. If you construct a new type involving an existing type then it is covariant if the construction follows the same the “use in place of” relationship. If there is no relationship then the construct is invariant.

  • Inputs tend to be contravariant, outputs covariant and general read/write types are invariant.

  • Generics and arrays in Kotlin are invariant by default.

  • You can modify this default variance by using the in and out modifiers when you declare the type – declaration-site variance.

  • You can also modify the variance when you use a type using the same in and out modifiers – use-site variance or projections.

  • The * projection lets you pass any instance of a generic, no matter what its type.

 

This article is an extract from: 

Programmer's Guide To Kotlin

Now Available as a Print Book

cover

You can buy it from: Amazon

Contents

Some Chapters Already Available On The Web

  1. What makes Kotlin Special (Book Only)
  2. The Basics: Variables, Primitive Types and Functions
  3. Control
  4. Strings and Arrays
  5. The Class & The Object
  6. Inheritance
  7. The Type Hierarchy 
            Extract  Type and its problems
            Extract  Smart Casts
  8. Generics
            Extract Basic Generics
            Extract Covariance & Contravariance
  9. Collections, Iterators, Sequences & Ranges
            Extract     Collections
            Extract Iterators & Sequences
  10. Advanced functions 
  11. Anonymous, Lamdas & Inline Functions
            Extract    Annoymous and Lambda Functions
            ExtractInline Functions
  12. Data classes, enums and destructuring
            Extract  Data Classes
            Extract      Enums & Sealed Classes 
            Extract 
         Delegated Properties 
            Extract      Destructuring  **NEW!**
  13. Exceptions, Annotations & Reflection
            Extract Exceptions
            Extract Annotation & Reflection 
  14. Working with Java
            Extract Using Swing  

To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on, Twitter, Facebook or Linkedin.

Banner


Too Good To Miss: Salto The Bouncing Robot
07/02/2021

Some of our news items deserve a second chance. Here's one from June that is simply worth seeing. Salto is a tiny bouncing robot that proves that there is more than one way to build a robot. You can't [ ... ]



Rust Team Announces Rust Foundation
11/02/2021

The Rust Core team has announced the start of the active life of the Rust Foundation, a new independent non-profit organization to steward the Rust programming language and ecosystem. A major focus wi [ ... ]


More News

square

 



 

Comments




or email your comment to: comments@i-programmer.info

<ASIN:1871962536>

<ASIN:1871962544>



Last Updated ( Monday, 21 January 2019 )