Tuesday, May 10, 2016

The Significance of Kotlin for Android

Introduction

To this point of its existence, Android has leveraged Java as its development language of choice. While the selection of Java no doubt opened the framework up to a large pool of developers and the extensive ecosystem of existing libraries and frameworks, that decision as of late is beginning to show its wrinkles:
  • Android isn't compatible with the latest version of Java... which was released well over two years ago. Java 8 was released in March of 2014. Java 7, the latest version compatible(-ish) with Android, reached end-of-life status in April of 2015... which in some sense means Android itself is currently in an end-of-life state.
  • Given that, the latest additions to Java 8 aren't readily available, if at all, including:
    • lambdas and their related features (method references, higher-order functions, etc)
    • streams
    • an improved calendar API
    • an improved type inference system
While there may be backport libraries available to address many of these shortcomings, and if you're able to overlook the annoyance of having to bolt on libraries for what are standard features as of Java 8, some of the commonplace problems with the language itself endure:
  • its verbosity makes for fertile ground for bugs while also hindering its readability 
  • the inability to add behavior to existing 3rd-party types adds to the verbosity
  • the capturing nature of anonymous inner-classes are a common source of Context leaks in Android
  • exception handling idioms promote poor handling of null values
  • mutability by default
Ironically, Java's admirable insistence on backward compatibility means that unless we start changing the way we write Java code, these problems aren't going anywhere anytime soon.

Enter Kotlin.

Kotlin

Kotlin is a statically typed, compiled language targeting the JVM. (It can also target Javascript for those looking to do end-to-end client-server development). Kotlin's standard library offers Java developers many language features aimed at providing an improved development experience, while also offering interoperability with Java.

To get a sense of just how semantically rich Kotlin code can be, have a look at this small snippet of code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
data class Car(val makeModel: String, var miles: Int? = null)

fun main(args: Array<String>) {
    val cars = listOf(
        Car("Nissan Sentra", 150000), 
        Car("Honda Civic"),
        Car("Lexus ES-350", miles = 16000))
    
    val fewestMiles = cars.minBy { it.miles ?: Int.MAX_VALUE }
    
    println("The car with the fewest miles is the ${fewestMiles?.makeModel}")
}

A line-by-line examination:
  • line 1 -
    • data classes provide a few generated boilerplate goodies out of the box
      • sensible equals(), hashCode(), and toString() implementations based on initialization parameters
      • a copy() function supporting property overrides via named parameters
      • generated componentN() methods to support destructuring declarations
    • the val keyword preceding makeModel effectively stores the parameter as private and final and an accompanying accessor method is synthesized
    • the var keyword preceding miles effectively stores the parameter as private and accompanying accessor and mutator methods are synthesized
  • line 3 - standalone functions as first-class types
  • line 4 - immutable references (mutable references as well, though not demonstrated above)
  • line 6 - defaultable parameter values
  • line 7 - named parameters
  • line 9 - 
    • higher-order functions
    • enhanced collections and extension functions (more on this later)
    • the Elvis operator (?:) provides a concise syntax for supplying alternative values to null
  • line 11 -
    • in-line string interpolation (as opposed to token-based or string concatenation)
    • concise syntax for null-safe operations
That's quite a few features demonstrated in just 12 lines of code, and that's just scratching the surface.

I've been using Kotlin in my day-to-day for a few months now. Here are three features that have thus far stood out to me.

Statically Checked Null Types

Not the sexiest feature, but probably the one from which the accuracy of my code's error handling benefitted the most.

In Kotlin, a variable that can potentially hold a null reference must be explicitly declared with a ? suffixing the type declaration:

1
2
var cannotBeNull: String = ""
var canBeNull: String? = null

The compiler recognizes those are two distinct types:
  • String, and
  • String?
Therefore a compiler error will result if one type is used in place of the other, no different than, say, trying to pass an Int where a String is expected.

A nullable type can be cast to its non-null variant in one of two ways.
  1. In the favorable, idiomatic approach, the reference is explicitly checked for null. If the code is running in a branch of execution where the compiler infers that the reference cannot possibly be null, it is then treated as its non-nullable counterpart. This feature is referred to as "smart casting".  (See lines 11-13 in the below snippet).
  2. The second way to cast a nullable type to its non-nullable counterpart is to apply the unary !! operator.  This operator returns the non-null value if a value is present or otherwise throws a NPE. Use this sparingly as a workaround of last resorts and only in cases where you absolutely know a value cannot be null, or if you just like NPEs. (See line 17 in the below snippet).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun computeSomeMessage(): String? { ... }

fun saySomething(message: String) {
  println("I would just like to say, $message")
}

val message = computeSomeMessage()  // <-- type is String?

saySomething(message)  // <-- will not compile

if(message != null) {
  saySomething(message)  // <-- compiles; message was smart casted
}

// message is forcibly casted to a non-nullable String
// this could result in a NPE
saySomething(message!!)

Kotlin's approach to handling null values is even more favorable than Java 8's Optionals (if they were even available for Android) because, while Optionals provide structured semantics for handling possible null values, a handle to an Optional itself can still be null.

Ultimately this means putting in more work around handling possible null values in your application, but better to make those decisions at compile-time rather than your users suffering unexpected app crashes.

Functions, Higher-order Functions, and Inline Functions

Where classes should be used to abstract over state, functions should be used to abstract over actions. Up until Java 8, Java developers have been missing out a concise way to express the latter. What has existed is anonymous classes. However, using anonymous classes in Android gave rise to a common class of program errors due to one easily overlooked aspect of Java's anonymous classes - they capture the variables available to them through their enclosing scope (its closure). Thus, if an anonymous class instance is created in an Activity, for example, that Activity's associated Context is captured in the anonymous class instance. As long as that anonymous class instance lives on, so does the Context and all the references it retains, until it is garbage collected or the app eventually dies due to an eventual OutOfMemoryError.

As mentioned earlier, Kotlin indeed offers first class support for lambdas and higher-order functions (functions that accept function literals as arguments or return function literals). The emitted bytecode behind lambdas looks no different than that of an anonymous class instance - every time you use one, an extra class and object are created. Not great for performance. But as an improvement over Java's anonymous classes, Kotlin's lambdas only capture if they close over references in the enclosing scope. In other words, if a lambda is defined with in an Activity, but does not reference any of its members, it will not retain an reference to the Activity, and therefore you need not worry about leaking the Context.

With regards to the performance implications mentioned in the previous paragraph, functions can be inlined. The inline declaration instructs the compiler to replace the call to the function with the actual code implementing the function rather than generating another anonymous class instance, thereby giving you all the benefits of using lambdas without sacrificing performance. This does have its limitations, however. If a higher-order function accepts another function as a parameter and that function is simply called, then that higher-order function may be inlined. However, if a reference to the function parameter is retained, it may not be inlined since there must be a generated object in order to retain that state.

Lastly, another nice little feature is that Kotlin allows us to substitute lambadas where SAM interfaces are used. This helps improve the overall readability of the code, and therein its supportability.

1
2
3
4
5
6
7
8
// Java 
button.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) { /* do something... */ }
});

// Kotlin
button.setOnClickListener { /* do something... */ }

Extension Functions

Extension functions grant us the ability to add behavior to existing classes without the need to extend, decorate, etc. This even applies to classes outside of our control (e.g. the core JDK).

As an example, I prefer read and write code that reads as fluently as prose. So code like this tends to not sit well with me:

1
2
3
4
5
val future: Future<Foo> = executor.submit(...)

if(!future.isDone() && !future.isCancelled()) {
  // do something...
}

I find the use of the negation operator (line 3) in these cases to be both an unnatural way to read as well as easy to overlook when scanning through code. A more fluent way to rewrite this API would be to introduce a new isRunning() extension function on the JDK's Future interface.

1
2
3
4
5
6
7
fun <T> Future<T>.isRunning() = !this.isDone && !this.isCancelled

val future: Future<Foo> = executor.submit(...)

if(future.isRunning()) {
  // do something...
}

This is obviously a trivial example, but hopefully the convenience of the feature is apparent.

My only worry with this this feature is that, if abused, newcomers to a codebase may find it confusing and frustrating to have to discover new behaviors and properties added to well-known classes at the whims of one rogue developer. I had such an experience working on a Scala project that used a lot of implicit conversions, and as a newcomer to the project and the language, it really impeded my ability to grasp what was going on.