pretty

Wednesday, 24 February 2021

Kotlin contravariance samples

Contravariance is used to allow adding into collection values of type T and T subtypes. The collection itself, however, is allowed to contain items of type T and any super types of T (because of this it is not safe to get from this collection, with the exception of getting the root of type hierarchy Any?).

1. Use site variance

Sample: list with contravariance acts as a consumer.
It is safe to add into list the object of declared type B, or it's subtypes. It is not safe to retrieve object of type B from the list, because list of objects with super types of B may be provided. However, it is safe to retrieve the Kotlin most basic type Any? from the list.
 
open class A
open class B : A()
class C : B()

// Can receive list of B or its super types (A, Any, Any? types in this sample).
// Allows to put B or B subtypes into list (C in this sample).
// Cannot get fom List - except, can only get most generic type Any? from list.

fun listAsConsumer(list: MutableList<in B>) {
     // Type mismatch: inferred type is Any? but B was expected
     // val b: B = list[0] 
     
     // Type mismatch: inferred type is A but B was expected 
     // list.add(A())  
     
     // Cannot get fom List - except, can only get most generic type Any? from list.
     val any: Any? = list[0]
     
     // Allowed to add B or its subtypes into list
     list.add(B())
     list.add(C())
}

fun main() { 
    listAsConsumer(mutableListOf<A>(A())) 
} 

Sample: contravariance in Comparator.
Compare list of Ints using Comparator<Number>.
fun <T> Iterable<T>.sortedWithWrapper(comparator: Comparator<in T>): List<T> {
    return this.sortedWith(comparator)
}

fun main() { 
    val comparator: Comparator<Number> = Comparator { o1: Number, o2: Number ->
        o1.toDouble().compareTo(o2.toDouble())
    }
    val intList: List<Int> = listOf(4, 7)
    val result: List<Int> = intList.sortedWithWrapper(comparator)
}

The same sample without contravariance for Comparator is below. Without contravariance sortedWithWrapper() function could not return List<Int>, but only List<Number>. This happens because List<Int> is upcasted to List<Number>, to match the invariant T of the Comparator.
fun <T> Iterable<T>.sortedWithWrapper(comparator: Comparator<T>): List<T> {
    return this.sortedWith(comparator)
}

fun main() { 
    val comparator: Comparator<Number> = Comparator { o1: Number, o2: Number ->
        o1.toDouble().compareTo(o2.toDouble())
    }
    val intList: List<Int> = listOf(4, 7)
    
    // Type mismatch: inferred type is List<Number> but List<Int> was expected
    // val result: List<Int> = intList.sortedWithWrapper(comparator)
    val result: List<Number> = intList.sortedWithWrapper(comparator)
}


2. Declaration site variance

Declaration site variance is available in Kotlin, unlike Java where only use-site variance is possible.

Sample: Class with contravariance. Consumer cannot have contravariant type T in invariant or out positions.
open class A
open class B : A() 
class C : B() 
 
class Consumer<in T> {
    
    // Type parameter T is declared as 'in' but occurs in 'invariant' 
    // position in type T?
    // var x: T? = null

    fun consume(x: T) {}  
     
    // Ok, List is defined as List<out T>,
    // so it is a producer for this Consumer class
    fun consume(x: List<T>) {} 
     
    // Type parameter T is declared as 'in' but occurs in 'invariant' position 
    // in type Array<T>
    // fun consume(x: Array<T>) {} 
     
    // Type parameter T is declared as 'in' but occurs in 'out' position
    // in type T?
    // fun produce() : T? { return x }   
}

fun main() {   
    // Consumer of B (can be assigned Consumer of supertypes (A, Any, Any?)),
    // but now minimum B or its subtypes are accepted into consume()
    val consumerB: Consumer<B> = Consumer<A>() 
    consumerB.consume(C())
    
    // Type mismatch: inferred type is A but B was expected
    // consumerC.consume(A()) 
    
    // Type mismatch: inferred type is Consumer<B> but Consumer<A> was expected
    // val consumerA: Consumer<A> = Consumer<B>()  
}

Consumer class with contravariant T cannot accept another consumer with contravariant T.
class AnotherConsumer<in T> {
     fun consume() {}
}

class Consumer<in T> {
   
    // Type parameter T is declared as 'in' but occurs in 'out' position
    // in type AnotherConsumer
    // fun consume(x: AnotherConsumer<T>) {}  
}

This is due to AnotherConsumer can be extended by the class that cancels the contravariant 'in' modifier:
open class AnotherConsumer<in T> {
     open fun consume(x: T) {}
}

class AnotherConsumerChild: AnotherConsumer<T>() {
    var x: T? = null
    
    override fun consume(x: T) {
        this.x = x
    }  
    
    fun getT(): T? {
        return x
    }
}

Allowing to accept another contravariant consumer would lead to ClassCastException.
As an example of the above, in this snippet AnotherConsumerC would be called with the wrong type A (omitting compiler variance checking with @UnsafeVariance annotation).
open class A
open class B : A() 
class C : B() {
    fun c() {}
} 

open class AnotherConsumer<in T> {
     open fun consume(x: T) {}
}

// Overrides contravariance in parent type
class AnotherConsumerC: AnotherConsumer<C>() {
    override fun consume(x: C) {
        x.c()
    }  
}

open class Consumer<in T> {  
    open fun consume(anotherConsumer: AnotherConsumer<@UnsafeVariance T>) {}  
}  

open class ConsumerA: Consumer<A>() {  
    override fun consume(anotherConsumer: AnotherConsumer<A>) {
        anotherConsumer.consume(A())
    }  
}  

fun main() {   
    val consumerA = ConsumerA()
    
    // Allowed because parent Consumer<in T> is contravariant
    val consumerC: Consumer<C> = consumerA
    
    // Produces Exception in thread "main" 
    // java.lang.ClassCastException: A cannot be cast to C
    consumerC.consume(AnotherConsumerC())
}

Wednesday, 24 July 2019

Android ScrollView child width is larger than the screen (Api 21 bug)


On Android Lollipop this layout would produce a screen where children views of the ScrollView would have incorrect width.

This is how the view looks on api level 21 emulator


And this the correct result, from api 27


To fix this ScrollView issue on api 21 devices, the child view "android:layout_margin" should be replaced with "android:padding". If it is not acceptable for the layout to have padding instead of margin (for example due to background color or click area placement) - padding can be set on parent container views. rows="20"

Thursday, 31 January 2019

GoScores: leaderboard on Google Cloud


Simple Leaderboard server, written in Go language and hosted on Google Cloud. Source (Github).

Sample app that uses the leaderboard: Buildris on Google Play


Update: Migrating to Go 1.16 on Google Cloud.

Google Cloud no longer supports Go 1.11 runtime, the migration guide from Google is available.

Appengine google.golang.org/appengine/* packages were removed and google cloud cloud.google.com/go/* packages have to be used instead (or Go standard library packages were applicable).

For example:
google.golang.org/appengine/datastore -> cloud.google.com/go/datastore (from Google cloud library) google.golang.org/appengine/log -> log (from Go standard library package)

Sample diff of migration for GoScores app from 1.11 to 1.16, that uses Google Cloud datastore.

To highlight other migration changes:
  • Port should now be set explicitly
  • Data store API was modified, for example, query.GetAll(() is now dsClient.GetAll().
  • Some functions calls changed, for example RunInTransaction() no longer accepts nil for TransactionOptions. Passing nil would result in 503 Server Error and in logs: “panic serving … nil TransactionOption goroutine [running]:”

To test locally:
  • Credentials should be locally available, run export with a path to the file with key:
    export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json"
    Key is available for download in Cloud Console, IAM and Admin -> Service accounts -> Keys tab. https://cloud.google.com/docs/authentication/production
  • go run scores.go
  • check GET response on localhost:8080


To deploy:
Inside app dir call (given google-cloud-sdk directory is one level up) ../google-cloud-sdk/bin/gcloud app deploy

-----
Full logs of app execution and errors are available in Logs Explorer: https://console.cloud.google.com/logs