Where will this be useful to me in life or using Nothing in Kotlin as an example

What is this article about?

In this article I want to show why a developed type system in a programming language is great. I'll tell you in detail how the class works. Either from the library ArrowI will analyze the features of the Kotlin type system – sealed hierarchies, covariance and Nothing, without which solving the problem becomes almost impossible.

I will try to conduct a small educational program about such seemingly complex things using a clear, practical example.

What is Either?

Either is a class that contains a value of one of two types. Essentially this is the type or A, or B (either A or B). Further in the text I will focus on the phrase ororto emphasize that we are talking about types of the Either class.

Important: Do not confuse Either with the Pair type, because a pair contains two values ​​at the same time, and Either contains a single value of one of two types.

Where can it be useful? Let's understand it with an example.
Let's say we make a trip to a service using the API and want to return or useful data, or Report an error if something goes wrong during a call.

data class Data(val payload: String)

enum class Error {
    UNAUTHORIZED,
    FORBIDDEN,
    BAD_REQUEST,
    INTERNAL_ERROR;
}

interface MyService {
    fun getData(): Either<Error, Data> 
}

We have defined a Data class that contains useful data that is returned if everything went without errors.
The Error class, which contains a list of possible errors.
MyService interface with getData function that returns or data, or error.

By convention, the error type is usually on the left and is called Left, and the payload type is called Right and is on the right (thanks, cap).

It would seem, what is the problem? It is possible to implement a type simply as a pair with constraint checking in the constructor.

class Either<A : Any, B : Any>(val a: A?, val b: B?) {
  init {
    require(a == null && b == null || a != null && b != null) {
      "Either should have only one value!"
    }
  }
}

The problem with this solution is that it does not provide type safety. We want to ensure that it is provided exactly one meaning, and there is no other. In the example above, this property is checked at runtime and an exception is thrown if it is violated.
Another implementation problem above is the use of null to represent a missing value. This limits the scope of our class and prohibits the use of optional types.
If we write a type-safe implementation of this class, we can guarantee the program properties we need at the compilation stage, and not at runtime, which reduces the number of errors during development.

Let's look at the techniques needed to build a proper Either class.

Sealed classes and interfaces

Let's start with an important feature of the Kotlin type system – sealed class hierarchies (aka algebraic data types). Declaring a class or interface with the sealed modifier prevents the creation of class descendants (or interface implementations) in other modules and makes the hierarchy fully known at compile time.

This, for example, allows the compiler to check branches of the when expression.

sealed interface Animal {
  fun say()
}

class Cat : Animal {
  override fun say() = println("meow")
    
  fun purr() = println("cat can purr")
}

class Dog : Animal {
  
  override fun say() = println("woof")

  fun howl() = println("dog can howl")
}

fun test(animal: Animal) {
  when (animal) {
    is Cat -> animal.purr()
    is Dog -> animal.howl()
  }
}

fun main() {
  test(Cat())
  test(Dog())
}

In this example, due to the sealed class hierarchy, the compiler understands that the when expression has examined all possible options and there is no need for an else branch. Moreover, if some new heir appears, an error will occur at the compilation stage, and not at runtime.

Let's return to our Either class. It has only two states – either the value of class A or the value of class B. Let's try to represent it as a sealed class.

sealed class Either<A, B> {
  class Left<A>(val a: A)//: Either<A, ???>
  class Right<B>(val b: B)//: Either<???, B>
}

Now our class has two heirs (states) and we are halfway to solving the problem. However, now they are not descendants of Either, because they do not implement a type for the value B. The problem arises – how to provide exactly one value, and even of any type specified by the user? To solve this problem, type covariance comes to the rescue.

Type covariance (in/out)

Type covariance (as well as invariance and contravariance) is used in classes with generic parameters and shows exactly how they are inherited from each other.

In the example below, we declare the Desk class with a covariant parameter User. This allows assignments of the Desk class to any subclass of the User class where Desk is expected.

open class User
class Manager : User()
class Engineer : User()

class Desk<out U : User>(val user: U)

fun main() {
  val desk: Desk<User> = Desk(Manager())
}

Any? Nothing.

Another feature of the type system in Kotlin is the presence of the Any and Nothing types.

Everyone is more or less familiar with Any – it is a type that is the parent of any other type. But the Nothing type, its exact opposite, is less common in modern programming languages. Nothing is a type that is a descendant of any type.

In itself, this does not make sense – it is clear that it is impossible to inherit all classes at once, so an instance of Nothing cannot be created. The class itself has a private constructor, and the type means a value that does not exist. For example, a function that always throws an exception will have a return type of Nothing.

public class Nothing private constructor()

Then the class relationship from the previous example looks like this:

And the hierarchy looks exactly the same for classes with the generic Desk parameter due to covariance.

Putting it all together

So now we know how to represent a value that doesn't exist and how to allow a subtype to be substituted for the original type through covariance.
Now, armed with everything we need, let’s write our final implementation of Either.

sealed class Either<out A, out B> {
  class Left<A>(val value: A): Either<A, Nothing>()
  class Right<B>(val value: B): Either<Nothing, B>()
}

fun main() {
    val a: Either<Int?, String> = Either.Left(null)
    val b: Either<Int, String> = Either.Right("string")
}

First about use. The example shows that Either can be used or Either.Left, or Either.Right. We can provide one of the values ​​without providing the other.

Now, let's look at three key points why everything works this way:

  1. sealed class Either – our Either class has only two possible states and cannot have others. The sealed class is perfect for implementing this property and guaranteeing it at the compilation stage.

  2. <out A, out B> – this construction allows you to substitute any of their heirs (including Nothing) instead of A and B themselves

  3. Either<A, Nothing>() – it is precisely through Nothing that it is guaranteed that if we have a value A, then there cannot be a value B and vice versa. The value of one of the types is Nothing.

It is the combination of these three techniques together that makes it possible to implement a type-safe Either class.

Conclusion

We looked at several important aspects of the Kotlin type system and were able to write our own type-safe implementation of the Either class. This is my first article, so I will be glad for any feedback 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *