The Programmers Guide To Kotlin - Generics
Written by Mike James   
Monday, 04 June 2018
Article Index
The Programmers Guide To Kotlin - Generics
Generic Constraints

Supplying Generic Actions

So how can generics implement anything useful if you cannot apply any type specific actions to generic entities?

One very general answer is to use implementations of a generic function type that implements the operation you require on particular types. 

For example, you can't use a+b when the types are unknown but you can rewrite a custom add function as:

fun <T> add(a:T,b:T,op:(T,T)->T):T{
        return op(a,b)
    }

Notice now that the third parameter is a function that takes two parameters of type T and returns a T. This is a generic formulation of a function that takes two parameters of the same type and applies a function to them that that is also passed as a parameter. The function that is passed in its turn takes two parameters of the specified type and returns a result of the same type. All of this is fine as no knowledge of the type T is used in this – it works for any type including Any.

Now lets try adding two Ints:

add<Int>(1,2,sumInt)

The first two parameters are fine as they are Ints but the third isn't defined and it needs to be a function that accepts two Ints and returns an Int.

This is easy to define:

var sumInt: (Int,Int)->Int = {a,b-> a+b}

This is a lambda, see Chapter 11. The lambda is defined within the curly brackets and it is a function of the correct type i.e. (T,T)→T where T is Int.

Now everything works and:

add<Int>(1,2,sumInt)

returns three. Notice that the function you pass is not a generic – it has a definite type even at compile time.

The downside is that you need to define an auxiliary  function for each of the types that you actually want to process. 

You can make this look more like a fully generic solution by defining a typealias:

typealias arithmetic<T> =(T,T)->T var sumInt:arithmetic<Int> = {a,b-> a+b} fun <T> add(a:T,b:T,op:arithmetic<T>):T{
        return op(a,b)
    }

Once again the sumInt is fully defined at compile time including the type of its parameters.

Generic Constraints

The problem is that when we use a type parameter like T it can be any type. This is the advantage of generics but it means that, if we want to use any operations on a particular type we have to implement specific functions that work with the type. 

The reason we can't call any methods on a generic type is that the compiler has no idea what the type is at runtime. We can relax this by applying generic constraints to the type parameter which limits what sort of types are allowable. Knowing that the type parameter must be a particular type or a subtype of that type we can allow methods to be used secure in the knowledge that the methods will exist at runtime. 

The only constraints that Kotlin provides are generally called "upper bounds". You can follow the type parameter with a type specifier. This gives the base class for the set of types that the type parameter can be.

In other words the type parameter has to be either the specified base class or a class derived from it. This allows the compiler to infer that an object described by the type parameter can have any of the methods of the base class and so these can be used in the generic. 

For example:

fun <T>:MyClassA myFunction(a:T):T{...

defines a function that accepts an object of type MyClassA as a parameter or anything derived from it. Notice that this is the same as 

fun myFunction(a:MyClassA){..

as a derived class can be used in place of a base class.

Also notice that constraints have the same problem as using a base class as a type. If the base class doesn't implement the methods you want to use then you can't make use of them.

For example, you still can't implement a generic add function because Number doesn't define an addition function. That is

fun <T>:Number add(T,T):T{
 return T+T
}

doesn't work because Number doesn't define an addition operation. Even with constraints you are still at the mercy of the way the class hierarchy is constructed in what you can easily implement as a generic. 

As well as a single base class constraint you can also specify multiple constraints using where. For example:

fun <T> myMax(a:T,b:T):T
     where  T:Number,
            T:Comparable<T>{
        return if(a>b) a else b
    }

In this case T has to be derived from the Number class and also implement the Comparable interface. All of the usual number classes satisfy this constraint. With the constraint in place we can use the greater than operator to return the maximum.

Also notice that Comparable is itself a generic interface and this form of constraint would be difficult to implement in any other way. 

This is more powerful but notice that we still can't implement a generic sum function because there isn't a Summable interface. Each of the Numeric types implement their own plus function and it would seem reasonable that any Number type would have a plus function but it isn't defined in Number or in a Summable interface.

This is a completely general problem. If you want to write a generic that works with all of the derived classes of an upper bound then the class that forms the upper bound has to have all of the methods you want to use.

 

cover

Co and Contravariance

final version in book

Co & Contra-variant Generics

final version in book

Controlling Variance – In and Out

final version in book

Type Projections

final version in book

The * Projection

final version in book 

Summary 

(Italicized text refers to material not included in this  extract)

  • 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 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 runtime 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 runtime.

  • 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 Third Edition

kotlin3e360

You can buy it from: Amazon

Contents

  1. What makes Kotlin Special
  2. The Basics:Variables,Primitive Types and Functions 
  3. Control
         Extract: If and When 
  4. Strings and Arrays
  5. The Class & The Object ***NEW!
  6. Inheritance
  7. The Type Hierarchy
  8. Generics
  9. Collections, Iterators, Sequences & Ranges
        Extract: Iterators & Sequences 
  10. Advanced functions 
  11. Anonymous, Lamdas & Inline Functions
  12. Data classes, enums and destructuring
        Extract: Destructuring 
  13. Exceptions, Annotations & Reflection
  14. Coroutines
        Extract: Coroutines 
  15. Working with Java
        Extract: Using Swing
  16. Compose Multiplatform
        Extract: Compose Layout 

<ASIN:B0D8H4N8SK>

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


TestSprite Announces End-to-End QA Tool
14/11/2024

TestSprite has announced an early access beta program for its end-to-end QA tool, along with $1.5 million pre-seed funding aimed at accelerating product development, expanding the team, and scaling op [ ... ]



Azul Outperforms OpenJDK By Up To 37%
23/10/2024

Azul has announced that its Azul Platform Prime outperforms comparable OpenJDK distributions by as much as 37%. The company has also launched the Azul Java Performance Engineering Lab (JPEL) aimed at  [ ... ]


More News

espbook

 

Comments




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



Last Updated ( Monday, 04 June 2018 )