Functional Kotlin

4.7 (3 reviews total)
By Mario Arias , Rivu Chakraborty
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Kotlin – Data Types, Objects, and Classes

About this book

Functional programming makes your application faster, improves performance, and increases your productivity. Kotlin supports many of the popular and advanced functional features of functional languages. This book will cover the A-Z of functional programming in Kotlin. This book bridges the language gap for Kotlin developers by showing you how to create and consume functional constructs in Kotlin. We also bridge the domain gap by showing how functional constructs can be applied in business scenarios. We’ll take you through lambdas, pattern matching, immutability, and help you develop a deep understanding of the concepts and practices of functional programming. If you want learn to address problems using Recursion, Koltin has support for it as well. You’ll also learn how to use the funKtionale library to perform currying and lazy programming and more. Finally, you’ll learn functional design patterns and techniques that will make you a better programmer.By the end of the book, you will be more confident in your functional programming skills and will be able to apply them while programming in Kotlin.

Publication date:
February 2018
Publisher
Packt
Pages
350
ISBN
9781788476485

 

Chapter 1. Kotlin – Data Types, Objects, and Classes

In this chapter, we'll cover Kotlin's type system, object-oriented programming (OOP) with Kotlin, modifiers, destructuring declarations, and more.

Kotlin is, primarily, an OOP language with some functional features. When we use OOP languages to resolve problems, we try to model the objects that are a part of our problem in an abstract way with the information that is relevant to the problem.

If we're designing an HR module for our company, we'll model employees with state or data (name, date of birth, social security number, and others) and behavior (pay salary, transfer to another division, and others). Because a person can be very complex, there is information that isn't relevant for our problem or domain. For example, the employee's favorite style of bicycle isn't relevant for our HR system, but it is very relevant for an online cycling shop.

Once we identify the objects (with data and behavior) and the relationship with other objects of our domain, we can start developing and writing the code that we'll make a part of our software solution. We'll use language constructs (construct is a fancy way to say allowed syntax) to write the objects, categories, relationships, and so on.

Kotlin has many constructs that we can use to write our programs and, in this chapter, we'll cover many of those constructs, such as:

  • Classes
  • Inheritance
  • Abstract classes
  • Interfaces
  • Objects
  • Generics
  • Type alias
  • Null types
  • Kotlin's type system
  • Other types
 

Classes


Classes are the foundational type in Kotlin. In Kotlin, a class is a template that provides a state, a behavior, and a type to instances (more on that later).

To define a class, only a name is required:

class VeryBasic

VeryBasic is not very useful, but is still a valid Kotlin syntax.

The VeryBasic class doesn't have any state or behavior; nonetheless, you can declare values of type VeryBasic, as shown in the following code:

fun main(args: Array<String>) {
    val basic: VeryBasic = VeryBasic()
}

As you can see, the basic value has a VeryBasic type. To express it differently, basic is an instance of VeryBasic.

In Kotlin, types can be inferred; so, the previous example is equivalent to the following code:

fun main(args: Array<String>) {
    val basic = VeryBasic()
}

By being a VeryBasic instance, basic has a copy of the VeryBasic type's state and behavior, namely, none. So sad.

Properties

As discussed previously, classes can have a state. In Kotlin, a class's state is represented by properties. Let's have a look at the blueberry cupcake example:

class BlueberryCupcake {
  var flavour = "Blueberry"
}

The  BlueberryCupcake class has an has-a property flavour of type String.

Of course, we can have instances of the BlueberryCupcake class:

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    println("My cupcake has ${myCupcake.flavour}")
}

Now, because we declare the flavour property as a variable, its internal value can be changed at runtime:

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    myCupcake.flavour = "Almond"
    println("My cupcake has ${myCupcake.flavour}")
}

That is impossible in real life. Cupcakes do not change their flavor (unless they become stale). If we change the flavour property to a value, it cannot be modified:

class BlueberryCupcake {
    val flavour = "Blueberry"
}

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    myCupcake.flavour = "Almond" //Compilation error: Val cannot be reassigned
    println("My cupcake has ${myCupcake.flavour}")
}

Let's declare a new class for almond cupcakes:

class AlmondCupcake {
    val flavour = "Almond"
}

fun main(args: Array<String>) {
    val mySecondCupcake = AlmondCupcake()
    println("My second cupcake has ${mySecondCupcake.flavour} flavour")
}

There is something fishy here. BlueberryCupcake and AlmondCupcake are identical in structure; only an internal value is changed.

In real life, you don't have different baking tins for different cupcake flavors. The same good quality baking tin can be used for various flavors. In the same way, a well-designed Cupcake class can be used for different instances:

class Cupcake(flavour: String) { 
  val flavour = flavour
}

The Cupcake class has a constructor with a parameter, flavour, that is assigned to a flavour value.

Because this is a very common idiom, Kotlin has a little syntactic sugar to define it more succinctly:

class Cupcake(val flavour: String)

Now, we can define several instances with different flavors:

fun main(args: Array<String>) {
    val myBlueberryCupcake = Cupcake("Blueberry")
    val myAlmondCupcake = Cupcake("Almond")
    val myCheeseCupcake = Cupcake("Cheese")
    val myCaramelCupcake = Cupcake("Caramel")
}

Methods

In Kotlin, a class's behavior is defined by methods. Technically, a method is a member function, so, anything that we learn about functions in the following chapters also applies to the methods:

class Cupcake(val flavour: String) {
  fun eat(): String {
    return "nom, nom, nom... delicious $flavour cupcake"
  }
}

The eat() method returns a String value. Now, let's call the eat() method, as shown in the following code:

fun main(args: Array<String>) {
    val myBlueberryCupcake = Cupcake("Blueberry")
    println(myBlueberryCupcake.eat())
}

The following expression is the output of the preceding code:

Nothing mind-blowing, but this is our first method. Later on, we'll do more interesting stuff.

 

Inheritance


As we continue modelling our domain in Kotlin, we realize that specific objects are quite similar. If we go back to our HR example, an employee and a contractor are quite similar; both have a name, a date of birth, and so on; they also have some differences. For example, a contractor has a daily rate, while an employee has a monthly salary. It is obvious that they are similar—both of them are people; people is a superset where both contractor and employee belong. As such, both have their own specific features that make them different enough to be classified into different subsets.

This is what inheritance is all about, there are groups and subgroups and there are relationships between them. In an inheritance hierarchy, if you go up in the hierarchy, you will see more general features and behaviors, and if you go down, you will see more specific ones. A burrito and a microprocessor are both objects, but they have very different purposes and uses. 

Let's introduce a new Biscuit class:

class Biscuit(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour biscuit" 
  } 
}

Again, this class looks almost exactly same as Cupcake. We could refactor these classes to reduce code duplication:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) 
class Biscuit(flavour: String): BakeryGood(flavour)

We introduced a new BakeryGood class, with the shared behavior and state of both Cupcake and Biscuit classes and we made both classes extend BakeryGood. By doing so, Cupcake (and Biscuit) has an is-a relationship with BakeryGood now; on the other hand, BakeryGood is the Cupcake class's super or parent class.

Note that BakeryGood is marked as open. This means that we specifically design this class to be extended. In Kotlin, you can't extend a class that isn't open.

The process of moving common behaviors and states to a parent class is called generalisation. Let's have a look at the following code:

fun main(args: Array<String>) {
    val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry")
    println(myBlueberryCupcake.eat())
}

Let's try out our new code:

Bummer, not what we were expecting. We need to refract it more:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  open fun name(): String { 
    return "bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "cupcake" 
  } 
} 

class Biscuit(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "biscuit" 
  } 
}

It works! Let's have a look at theoutput:

We declared a new method, name(); it should be marked as open, because we designed it to be optionally altered in its subclasses.

Modifying a method's definition on a subclass is called override and that is why the name() method in both subclasses is marked as override.

The process of extending classes and overriding behavior in a hierarchy is called specialisation.

Note

Rule of thumb Put general states and behaviors at the top of the hierarchy (generalisation), and specific states and behaviors in subclasses (specialisation).

Now, we can have more bakery goods! Let's have a look at the following code:

open class Roll(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "roll" 
  } 
} 

class CinnamonRoll: Roll("Cinnamon")

Subclasses can be extended too. They just need to be marked asopen:

open class Donut(flavour: String, val topping: String) : BakeryGood(flavour)
{
    override fun name(): String {
        return "donut with $topping topping"
    }
}

fun main(args: Array<String>) {
    val myDonut = Donut("Custard", "Powdered sugar")
    println(myDonut.eat())
}

We can also create classes with more properties and methods.

 

Abstract classes


So far, so good. Our bakery looks good. However, we have a problem with our current model. Let's look at the following code:

fun main(args: Array<String>) {
    val anyGood = BakeryGood("Generic flavour")
}

We can instantiate the BakeryGood class directly, which is too generic. To correct this situation, we can mark BakeryGood as abstract:

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  }

  open fun name(): String { 
    return "bakery good" 
  } 
}

An abstract class is a class designed solely to be extended. An abstract class can't be instantiated, which fixes our problem.

What makes abstract different from open?

Both modifiers let us extend a class, but open lets us instantiate while abstract does not.

Now that we can't instantiate, our name() method in the BakeryGood class isn't that useful anymore, and all our subclasses, except for CinnamonRoll, override it anyway (CinnamonRoll relays on the Roll implementation):

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  abstract fun name(): String 
}

A method marked as abstract doesn't have a body, just the signature declaration (a method signature is a way to identify a method). In Kotlin, a signature is composed of the method's name, its number, the type of parameters, and the return type.

Any class that extends BakeryGood directly must override the name() method. The technical term for overriding an abstract method is implement and, from now on, we will use it. So, the Cupcake class implements the name() method (Kotlin doesn't have a keyword for method implementation; both cases, method implementation, and method overriding, use the keyword override).

Let's introduce a new class, Customer; a bakery needs customers anyway:

class Customer(val name: String) {
  fun eats(food: BakeryGood) {
    println("$name is eating... ${food.eat()}")
  }
}

fun main(args: Array<String>) {
    val myDonut = Donut("Custard", "Powdered sugar")
    val mario = Customer("Mario")
mario.eats(myDonut)
}

The eats(food: BakeryGood) method takes a BakeryGood parameter, so any instance of any class that extends the BakeryGood parameter, it doesn't matter how many hierarchy levels. Just remember that we can instantiate BakeryGood directly.

What happens if we want a simple BakeryGood? For example, testing.

There is an alternative, an anonymous subclass:

fun main(args: Array<String>) {
    val mario = Customer("Mario")

    mario.eats(object : BakeryGood("TEST_1") {
        override fun name(): String {
            return "TEST_2"
        }
    })
}

A new keyword is introduced here, object. Later on, we'll cover object in more detail, but for now, it is enough to know that this is an object expression. An object expression defines an instance of an anonymous class that extends a type.

In our example, the object expression (technically, the anonymous class) must override the name() method and pass a value as the parameter for the BakeryGood constructor, exactly as a standard class would do.

Remember that an object expression is an instance, so it can be used to declare values:

val food: BakeryGood = object : BakeryGood("TEST_1") { 
  override fun name(): String { 
    return "TEST_2" 
  } 
} 

mario.eats(food)
 

Interfaces


The open and abstract classes are great for creating hierarchies, but sometimes they aren't enough. Some subsets can span between apparently unrelated hierarchies, for example, birds and great apes are bipedal, and both are animals and vertebrates, but they not directly related. That is why we need a different construct and Kotlin gives us interfaces (other languages deal with this problem differently).

Our bakery goods are great, but we need to cook them first:

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  fun bake(): String { 
    return "is hot here, isn't??" 
  } 

  abstract fun name(): String 
}

With our new bake() method , it will cook all our amazing products, but wait, donuts aren't baked, but fried.

What if we could move the  bake() method to a second abstract class, Bakeable? Let's try it in the following code:

abstract class Bakeable { 
  fun bake(): String { 
    return "is hot here, isn't??" 
  } 
} 

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable() { //Compilation error: Only one class may appear in a supertype list 
  override fun name(): String { 
    return "cupcake" 
 }
}

Wrong! In Kotlin, a class can't extend two classes at the same time. Let's have a look at the following code:

interface Bakeable { 
  fun bake(): String { 
    return "is hot here, isn't??" 
  } 
} 

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable { 
  override fun name(): String { 
    return "cupcake" 
  } 
}

However, it can extend many interfaces. An interface is a type that defines a behavior; in the Bakeable interface's case, that is the bake() method.

So, what are the differences between an open/abstract class and an interface?

Let's start with the following similarities:

  • Both are types. In our example, Cupcake has an is-a relationship with BakeryGood and has an is-a relationship with Bakeable.
  • Both define behaviors as methods.
  • Although open classes can be instantiated directly, neither abstract classes nor interfaces can.

Now, let's look at the following differences:

  • A class can extend just one class (open or abstract), but can extend many interfaces.
  • An open/abstract class can have constructors.
  • An open/abstract class can initialize its own values. An interface's values must be initialized in the classes that extend the interface.
  • An open class must declare the methods that can be overridden as open. An abstract class could have both open and abstract methods.

In an interface, all methods are open and a method with no implementation doesn't need an abstract modifier:

interface Fried { 
  fun fry(): String 
} 

open class Donut(flavour: String, val topping: String) : BakeryGood(flavour), Fried { 
  override fun fry(): String { 
    return "*swimming on oil*" 
  } 

  override fun name(): String { 
    return "donut with $topping topping" 
  } 
}

When should you use one or the other?:

  • Use open class when:
    • The class should be extended and instantiated
  • Use abstract class when:
    • The class can't be instantiated
    • A constructor is needed it
    • There is initialization logic (using init blocks)

Let's have a look at the following code:

abstract class BakeryGood(val flavour: String) {
init {
    println("Preparing a new bakery good") 
  } 

  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  abstract fun name(): String 
}
  • Use interface when:
    • Multiple inheritances must be applied
    • No initialized logic is needed

Note

My recommendation is that you should always start with an interface. Interfaces are more straightforward and cleaner; they also allow a more modular design. In the case that data initialization/constructors are needed, move to abstract/open.

As with abstract classes, object expressions can be used with interfaces:

val somethingFried = object : Fried { 
  override fun fry(): String { 
    return "TEST_3" 
  } 
}
 

Objects


We already covered object expressions, but there is more on objects. Objects are natural singletons (by natural, I mean to come as language features and not as behavior pattern implementations, as in other languages). A singleton is a type that has just one and only one instance and every object in Kotlin is a singleton. That opens a lot of interesting patterns (and also some bad practices). Objects as singletons are useful for coordinating actions across the system, but can also be dangerous if they are used to keep global state.

Object expressions don't need to extend any type:

fun main(args: Array<String>) {
    val expression = object {
        val property = ""

        fun method(): Int {
            println("from an object expressions")
            return 42
        }
    }

    val i = "${expression.method()} ${expression.property}"
    println(i)
}

In this case, the expression value is an object that doesn't have any specific type. We can access its properties and functions.

There is one restriction—object expressions without type can be used only locally, inside a method, or privately, inside a class:

class Outer {
    val internal = object {
        val property = ""
    }
}

fun main(args: Array<String>) {
    val outer = Outer()

    println(outer.internal.property) // Compilation error: Unresolved reference: property
}

In this case, the property value can't be accessed.

Object declarations

An object can also have a name. This kind of object is called an object declaration:

object Oven {
  fun process(product: Bakeable) {
    println(product.bake())
  }
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake("Almond")
    Oven.process(myAlmondCupcake)
}

Objects are singletons; you don't need to instantiate Oven to use it. Objects also can extend other types:

interface Oven {
  fun process(product: Bakeable)
}

object ElectricOven: Oven {
  override fun process(product: Bakeable) {
    println(product.bake())
  }
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake("Almond")
    ElectricOven.process(myAlmondCupcake)
}

Companion objects

Objects declared inside a class/interface can be marked as companion objects. Observe the use of companion objects in the following code:

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable {
  override fun name(): String { 
    return "cupcake" 
  } 

  companion object { 
    fun almond(): Cupcake { 
      return Cupcake("almond") 
    } 

    fun cheese(): Cupcake { 
      return Cupcake("cheese") 
    } 
  } 
}

Now, methods inside the companion object can be used directly, using the class name without instantiating it:

fun main(args: Array<String>) {
    val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry")
    val myAlmondCupcake = Cupcake.almond()
    val myCheeseCupcake = Cupcake.cheese()
    val myCaramelCupcake = Cupcake("Caramel")
}

Companion object's methods can't be used from instances:

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake.almond()
    val myCheeseCupcake = myAlmondCupcake.cheese() //Compilation error: Unresolved reference: cheese
}

Companion objects can be used outside the class as values with the name Companion:

fun main(args: Array<String>) {
    val factory: Cupcake.Companion = Cupcake.Companion
}

Alternatively, a Companion object can have a name:

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable {
    override fun name(): String {
        return "cupcake"
    }

    companion object Factory {
        fun almond(): Cupcake {
            return Cupcake("almond")
        }

        fun cheese(): Cupcake {
            return Cupcake("cheese")
        }
    }
}

fun main(args: Array<String>) {
    val factory: Cupcake.Factory = Cupcake.Factory
}

They can also be used without a name, as shown in the following code:

fun main(args: Array<String>) {
    val factory: Cupcake.Factory = Cupcake
}

Don't be confused by this syntax. The Cupcake value without parenthesis is the companion object; Cupcake() is an instance.

 

Generics


This section is just a short introduction to generics; later, we'll cover it in detail.

Generic programming is a style programming that focuses on creating algorithms (and collaterally, data structures) that work on general problems.

The Kotlin way to support generic programming is using type parameters. In a few words, we wrote our code with type parameters and, later on, we pass those types as parameters when we use them. 

Let's take, for example, our Oven interface:

interface Oven {
  fun process(product: Bakeable)
}

An oven is a machine, so we could generalize it more:

interface Machine<T> {
  fun process(product: T)
}

The Machine<T> interface defines a type parameter T and a method process(T).

Now, we can extend it with Oven:

interface Oven: Machine<Bakeable>

Now, Oven is extending Machine with the Bakeable type parameter, so the process method now takes Bakeable as a parameter.

 

Type alias


Type alias provides a way to define names of types that already exist. Type alias can help to make complex types easier to read, and can also provide other hints.

The Oven interface is, in some sense, just a name, for a Machine<Bakeable>:

typealias Oven = Machine<Bakeable>

Our new type alias, Oven, is exactly like our good old Oven interface. It can be extended and have the values of the type Oven.

Types alias also can be used to enhance information on types, providing meaningful names related to your domain:

typealias Flavour = String

abstract class BakeryGood(val flavour: Flavour) {

It can also be used on collections:

typealias OvenTray = List<Bakeable>

It can also be used with objects:

typealias CupcakeFactory = Cupcake.Companion
 

Nullable types


One of the main features of Kotlin is nullable types. Nullable types allow us to define if a value can contain or being null explicitly:

fun main(args: Array<String>) {
    val myBlueberryCupcake: Cupcake = null //Compilation error: Null can not be a value of a non-null type Cupcake
}

This isn't valid in Kotlin; the Cupcake type doesn't allow null values. To allow null values, myBlueberryCupcake must have a different type:

fun main(args: Array<String>) {
    val myBlueberryCupcake: Cupcake? = null
}

In essence, Cupcake is a non-null type and Cupcake? is a nullable type.

In the hierarchical structure, Cupcake is a subtype of Cupcake?. So, in any situation where Cupcake? is defined, Cupcake can be used, but not the other way around:

fun eat(cupcake: Cupcake?){
//  something happens here    
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake.almond()

    eat(myAlmondCupcake)

    eat(null)
}

Kotlin's compiler makes a distinction between instances of nullable and non-null types.

Let's take these values, for example:

fun main(args: Array<String>) {
    val cupcake: Cupcake = Cupcake.almond()
    val nullabeCupcake: Cupcake? = Cupcake.almond()
}

Next, we will invoke the eat() method on both nullable and non-null types:

fun main(args: Array<String>) {
    val cupcake: Cupcake = Cupcake.almond()
    val nullableCupcake: Cupcake? = Cupcake.almond()

    cupcake.eat() // Happy days
    nullableCupcake.eat() //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Cupcake?
}

Calling the eat() method on cupcake is easy as pie; calling eat() on nullableCupcake is a compilation error.

Why? For Kotlin, calling a method from a nullable value is dangerous, a potential NullPointerException (NPE from now on) could be thrown. So, to be safe, Kotlin marks this as a compilation error.

What happens if we really want to invoke a method or access a property from a nullable value?

Well, Kotlin provides you options to deal with nullable values, with a catch—all are explicit. In some sense, Kotlin is telling you, Show me that you know what you are doing.

Let's review some options (there are more options that we'll cover in the following chapters).

Checking for null

Check for null as a condition in the if block:

fun main(args: Array<String>) {
    val nullableCupcake: Cupcake? = Cupcake.almond()

    if (nullableCupcake != null) {
      nullableCupcake.eat()
    }
}

Kotlin will do a smart cast. Inside the if block, nullableCupcake is a Cupcake, not a Cupcake?; so, any method or property can be accessed.

Checking for non-null types

This is similar to the previous one, but it checks directly for the type:

if (nullableCupcake is Cupcake) {
  nullableCupcake.eat()
}

It also works with when:

when (nullableCupcake) {
  is Cupcake -> nullableCupcake.eat()
}

Both options, checking for null and non-null types, are a little bit verbose. Let's check other options.

Safe calls

Safe calls let you access methods and properties of nullable values if the value isn't null (under the hood, at the bytecode level, a safe call is transformed into if(x != null)):

nullableCupcake?.eat()

But, what if you use it in an expression?

val result: String? = nullableCupcake?.eat()

It will return null if our value is null, so result must have a String? type.

That opens up the chance to use safe calls on a chain, as follows:

val length: Int? = nullableCupcake?.eat()?.length

The Elvis (?:) operator

The Elvis operator (?:) returns an alternative value if a null value is used in an expression:

val result2: String = nullableCupcake?.eat() ?: ""

If nullabluCupcake?.eat() is null, the ?: operator will return the alternative value "".

Obviously, the Elvis operator can be used with a chain of safe calls:

val length2: Int = nullableCupcake?.eat()?.length ?: 0

The (!!) operator

Instead of a null value, the !! operator will throw an NPE:

val result: String = nullableCupcake!!.eat()

If you can deal with an NPE, the !! operator gives you a pretty convenient feature, a free smart cast:

val result: String = nullableCupcake!!.eat()

val length: Int = nullableCupcake.eat().length

If nullableCupcake!!.eat() doesn't throw an NPE, Kotlin will change its type from Cupcake? to Cupcake from the next line and onwards.

 

Kotlin's type system


Type systems are a set of rules that determine the type of a language construct.

A (good) type system will help you with:

  • Making sure that the constituent parts of your program are connected in a consistent way
  • Understanding your program (by reducing your cognitive load)
  • Expressing business rules
  • Automatic low-level optimizations

We have already covered enough ground to understand Kotlin's type system.

The Any type

All types in Kotlin extend from the Any type (hold on a second, actually this isn't true but for the sake of the explanation, bear with me).

Every class and interface that we create implicitly extends Any. So, if we write a method that takes Any as a parameter, it will receive any value:

fun main(args: Array<String>) {

    val myAlmondCupcake = Cupcake.almond()

    val anyMachine = object : Machine<Any> {
      override fun process(product: Any) {
        println(product.toString())
      }
    }

    anyMachine.process(3)

    anyMachine.process("")

    anyMachine.process(myAlmondCupcake)    
}

What about a nullable value? Let's have a look at it:

fun main(args: Array<String>) {

    val anyMachine = object : Machine<Any> {
      override fun process(product: Any) {
        println(product.toString())
      }
    }

    val nullableCupcake: Cupcake? = Cupcake.almond()

    anyMachine.process(nullableCupcake) //Error:(32, 24) Kotlin: Type mismatch: inferred type is Cupcake? but Any was expected
}

Any is the same as any other type and also has a nullable counterpart, Any?.  Any extends from Any?. So, in the end, Any? is the top class of Kotlin's type system hierarchy.

Minimum common types

Due to its type inference and expression evaluation, sometimes there are expressions in Kotlin where it is not clear which type is being returned. Most languages resolve this problem by returning the minimum common type between the possible type options. Kotlin takes a different route.

Let's take a look at an example of an ambiguous expression:

fun main(args: Array<String>) {
    val nullableCupcake: Cupcake? = Cupcake.almond()

    val length = nullableCupcake?.eat()?.length ?: ""
}

What type does length have? Int or String? No, length value's type is Any. Pretty logical. The minimum common type between Int and String is Any. So far, so good. Let's look at the following code now:

val length = nullableCupcake?.eat()?.length ?: 0.0

Following that logic, in this case, length should have the Number type (the common type between Int and Double), shouldn't it?

Wrong, length is still Any. Kotlin doesn't search for the minimum common type in these situations. If you want a specific type, it must be explicitly declared:

val length: Number = nullableCupcake?.eat()?.length ?: 0.0

The Unit type

Kotlin doesn't have methods with void return (as Java or C do). Instead, a method (or, to be precise, an expression) could have a Unit type.

A Unit type means that the expression is called for its side effects, rather than its return. The classic example of a Unit expression is println(), a method invoked just for its side effects.

Unit, like any other Kotlin type, extends from Any and could be nullable. Unit? looks strange and unnecessary, but is needed to keep consistency with the type system. Having a consistent type system have several advantages, including better compilation times and tooling:

anyMachine.process(Unit)

The Nothing type

Nothing is the type that sits at the bottom of the entire Kotlin hierarchy. Nothing extends all Kotlin types, including Nothing?.

But, why do we need a Nothing and Nothing? types?

Nothing represents an expression that can't be executed (basically throwing exceptions):

val result: String = nullableCupcake?.eat() ?: throw RuntimeException() // equivalent to nullableCupcake!!.eat()

On one hand of the Elvis operator, we have a String. On the other hand, we have Nothing. Because the common type between String and Nothing is String (instead of Any), the value result is a String.

Nothing also has a special meaning for the compiler. Once a Nothing type is returned on an expression, the lines after that are marked as unreachable.

Nothing? is the type of a null value:

val x: Nothing? = null

val nullsList: List<Nothing?> = listOf(null)
 

Other types


Classes, interfaces, and objects are a good starting point for an OOP type system, but Kotlin offers more constructs, such as data classes, annotations, and enums (there is an additional type, named sealed class, that we'll cover later).

Data classes

Creating classes whose primary purpose is to hold data is a common pattern in Kotlin (is a common pattern in other languages too, think of JSON or Protobuff).

Kotlin has a particular kind of class for this purpose:

data class Item(val product: BakeryGood,
  val unitPrice: Double,
  val quantity: Int)

To declare data class, there are some restrictions:

  • The primary constructor should have at least one parameter
  • The primary constructor's parameters must be val or var
  • Data classes can't be abstract, open, sealed, or inner

With these restrictions, data classes give a lot of benefits.

Canonical methods

Canonical methods are the methods declared in Any. Therefore, all instances in Kotlin have them.

For data classes, Kotlin creates correct implementations of all canonical methods.

The methods are as follows:

  • equals(other: Any?): Boolean: This method compares value equivalence, rather than reference.
  • hashCode(): Int: A hash code is a numerical representation of an instance. When hashCode() is invoked several times in the same instance, it should always return the same value. Two instances that return true when they are compared with equals must have the same hashCode().
  • toString(): String: A String representation of an instance. This method will be invoked when an instance is concatenated to a String.

The copy() method

Sometimes, we want to reuse values from an existing instance. The copy() method lets us create new instances of a data class, overriding the parameters that we want:

val myItem = Item(myAlmondCupcake, 0.40, 5)

val mySecondItem = myItem.copy(product = myCaramelCupcake) //named parameter

In this case, mySecondItem copies unitPrice and quantity from myItem, and replaces the product property.

Destructuring methods

By convention, any instance of a class that has a series of methods named component1(), component2() and so on can be used in a destructuring declaration.

Kotlin will generate these methods for any data class:

val (prod: BakeryGood, price: Double, qty: Int) = mySecondItem

The prod value is initialized with the return of component1(), price with the return of component2() , and so on. Although the preceding example use explicit types, those aren't needed:

val (prod, price, qty) = mySecondItem

In some circumstances, not all values are needed. All unused values can be replaced by (_):

val (prod, _, qty) = mySecondItem

Annotations

Annotations are a way to attach meta info to your code (such as documentation, configuration, and others).

Let's look at the following example code:

annotation class Tasty

An annotation itself can be annotated to modify its behavior:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tasty

In this case, the Tasty annotation can be set on classes, interfaces, and objects, and it can be queried at runtime.

For a complete list of options, check the Kotlin documentation.

Annotations can have parameters with one limitation, they can't be nullable:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tasty(val tasty:Boolean = true)

@Tasty(false)
object ElectricOven : Oven {
  override fun process(product: Bakeable) {
    println(product.bake())
  }
}

@Tasty
class CinnamonRoll : Roll("Cinnamon")

@Tasty
interface Fried {
  fun fry(): String
}

To query annotation values at runtime, we must use the reflection API (kotlin-reflect.jar must be in your classpath):

fun main(args: Array<String>) {
    val annotations: List<Annotation> = ElectricOven::class.annotations

    for (annotation in annotations) {
        when (annotation) {
            is Tasty -> println("Is it tasty? ${annotation.tasty}")
            else -> println(annotation)
        }
    }
}

Enum

Enum in Kotlin is a way to define a set of constant values. Enums are very useful, but not limited, as configuration values:

enum class Flour {
  WHEAT, CORN, CASSAVA
}

Each element is an object that extends the Flour class.

Like any object, they can extend interfaces:

interface Exotic {
  fun isExotic(): Boolean
}

enum class Flour : Exotic {
  WHEAT {
    override fun isExotic(): Boolean {
      return false 
    }
  },

  CORN {
    override fun isExotic(): Boolean {
      return false
    }
  },

  CASSAVA {
    override fun isExotic(): Boolean {
      return true
    }
  }
}

Enum can also have abstract methods:

enum class Flour: Exotic {
  WHEAT {
    override fun isGlutenFree(): Boolean {
      return false
    }

    override fun isExotic(): Boolean {
      return false
    }
  },

  CORN {
    override fun isGlutenFree(): Boolean {
      return true
    }

    override fun isExotic(): Boolean {
      return false
    }
  },

  CASSAVA {
    override fun isGlutenFree(): Boolean {
      return true
    }

    override fun isExotic(): Boolean {
      return true
    }
  };

  abstract fun isGlutenFree(): Boolean
}

Any method definition must be declared after the (;) separating the last element.

When enums are used with when expressions, Kotlin's compiler checks that all cases are covered (individually or with an else):

fun flourDescription(flour: Flour): String {
  return when(flour) { // error
    Flour.CASSAVA -> "A very exotic flavour"
  }
}

In this case, we're only checking for CASSAVA and not the other elements; therefore, it fails:

fun flourDescription(flour: Flour): String {
  return when(flour) {
    Flour.CASSAVA -> "A very exotic flavour"
    else -> "Boring"
  }
}
 

Summary


In this chapter, we covered the basics of OOP and how Kotlin supports it. We learned how to use classes, interfaces, objects, data classes, annotations, and enums. We also explored the Kotlin type system and saw how it helps us to write better and safer code.

In the next chapter, we will start with an introduction to functional programming.

About the Authors

  • Mario Arias

    Mario Arias is a software engineer and Spring certified instructor with more than 12 years of experience in software development and design, databases, training material design, and training delivery. He currently works as a software engineer in Manchester, UK, for Cake Solutions Ltd., a BAMTECH media company.

    Mario is well-known member of the Kotlin community and is part of the Arrow team, the group that developed and maintains the Arrow functional library. In his free time, he rides his bicycle and trains Brazilian Jiu-Jitsu.

    Browse publications by this author
  • Rivu Chakraborty

    Rivu Chakraborty is a Google Certified Android Developer, Caster.io Instructor and a Kotlin Evangelist. With over 6 years of work experience; he is currently working as a Sr. Software Engineer (Android) at BYJU'S The Learning App.

    Rivu considers himself a Kotlin and Android enthusiast, cum evangelist. He has been using Kotlin since December 2015. Rivu created the KotlinKolkata User Group and before moving out to Bangalore, he had been the lead organiser for both Kotlin Kolkata User Group and GDG Kolkata.

    Along with organising events, he also speaks at events/conferences in India, including DroidJam India (India's premiere Android Conference) and a couple of DevFests.

    Rivu has authored multiple books on Kotlin and Android Development.

    Browse publications by this author

Latest Reviews

(3 reviews total)
Quick delivery and easy ordering.
is easy to connect with my kindle :D
Compared to other book, This book is good for beginner and covers most aspect of Kotlin

Recommended For You

Hands-on Design Patterns with Kotlin

Make the most of Kotlin by leveraging design patterns and best practices to build scalable and high performing apps

By Alexey Soshin
Learning Concurrency in Kotlin

Take advantage of Kotlin's concurrency primitives to write efficient multithreaded applications

By Miguel Angel Castiblanco Torres
Hands-On Data Structures and Algorithms with Kotlin

Understand and solve complex computational problems and write efficient code with Kotlin

By Chandra Sekhar Nayak and 1 more
Mastering Kotlin

Explore popular language features, Java to Kotlin interoperability, advanced topics, and practical applications by building a variety of sample projects

By Nate Ebel