Simple Inversion of Control in Kotlin without Dependency Injection frameworks (Part 2)

The tl;dr version is available as an example under the name Typeclassless.

In Part 1 of this blog series I described a technique that used the static resolution of extension functions (extfuns from now on) to describe dependencies for a function without explicitly requiring them as parameters, by using types. These types, called Syntax, can be composed together to form new types with more functionality.

The best feedback I received mentioned how the approach seemed to work great on smaller scopes, although it would be more difficult to see it applied at a larger scale. This is because it wouldn't work on a classic OOP design with many inter-dependencies between classes, that DI frameworks can resolve and instantiate. Luckily, in Kotlin we're not limited to OOP!

I'll dedicate this entry to describe how to use this IoC technique to solve a real world use case we found while developing the FP library Λrrow.

First, we're going to add a new configuration dimension to our Syntax types by including generic parametrisation. For that it's key that we first understand what Typeclasses are.

Typeclasses

For those coming from a Java background we're used to seeing interfaces as a way of describing the capabilities of a class. I will split these interfaces in three types:

  • Those who were implemented by a class - your API description, module definition, clean ports, abstract base class

  • Those who are usually implemented as an anonymous class - callbacks and listeners, now being replaced with lambdas!

These two types are the most common and they're rarely parametrized to a generic. If you like the technical word they're mostly "monomorphic", in that they only work for a single type they're applied for.

The third type is the one that I'm interested in.

  • Those who are implemented outside a class and generically parametrized to a type, providing one capability associated to the class they're parametrized to

Let's look at the most common example: Comparator<A>. I will write my own version here:

interface Comparator<A> {
  fun compare(a: A, b: A): Int
}

This interface has a generic parameter A that defines the type they're associated to. As the signature suggests, it requires two separate objects of type A. This forces the implementor to not do it inside the class that parametrizes to A. If they try to, they'll find that they are either comparing two values inside the current object, or ignoring one of the parameters to compare to itself. Let's see these two wrong implementations, assuming a fake function compare for String:

data class User(val useeId: String): Comparator<User> {
  // Why do I need to create a new user to compare another two?
  fun compare(a: User, b: User): Int =
    a.userId.compare(b.userId)

  companion object {}
}


data class Company(val companyId: String): Comparator<Company> {
  // Warning: parameter a is not used!
  fun compare(_: User, b: User): Int =
    this.companyId.compare(b.companyId)

  companion object {}
}

So the correct implementation has to always be done outside of the class. In this case we'll add it to the class' companion to be easily discoverable:

fun User.Companion.comparator() = object: Comparator<User> {
  fun compare(a: User, b: User): Int =
    a.userId.compare(b.userId)
}

val ucomp = User.comparator()
ucomp.compare(User("123"), User("321"))

Understanding this third type we can state that:

An interface that defines its relation to classes by a generic parameter is known as a Typeclass. An implementation of a Typeclass for a concrete data or sealed class is called an Instance.

So knowing that we can say that what we've just written is defined as as an Instance of Comparator for User. If you're talking about instances it's implicit that you mean for a typeclass, so saying an Instance of typeclass Comparator is redundant.

Now that we know what a Typeclass is, let's look at several examples provided by Λrrow.

Typeclass primer

UPDATE (Apr 14th '18): This article was written for Arrow 0.6. Starting on Arrow 0.7 all typeclasses are defined as the Syntax objects of this article!

As typeclasses are an old construct, there are decades of existing body of work with hundreds of them. Each describes a single capability for a single type. Here are some of the simplest and most familiar typeclasses you will find:

Eq

Structural equality between two objects. Think equals().

@typeclass
interface Eq<in F> : TC {
  fun eqv(a: F, b: F): Boolean
}

Order

Determine whether one object precedes another, which means you should be able to compare them with Eq too. Think Comparator!

@typeclass
interface Order<F> : Eq<F>, TC {
  fun compare(a: F, b: F): Int
}

Show

Literal string representation of an object. Think toString().

@typeclass
interface Show<F> : TC {
  fun show(a: A): String
}

Hash

Obtains the hashcode of an object. Think hashCode().

@typeclass
interface Hash<F> : TC {
  fun hash(a: A): Int
}

Semigroup

Can combine two objects together. Think plus().

@typeclass
interface Semigroup<A> : TC {
    fun combine(a: A, b: A): A
}

Replacing class methods with typeclasses

Up until now you were only able to define equals() in a class, and you were on your own if you wanted to change the implementation if that class was final. The same applies to your hashing algorithm, or the string representation of a value.

By using typeclasses you can redefine any common functionality for any class, whether it's open, final, in or outside of your codebase. As they're like supercharged extension functions, you can think about them as extension types.

You can define as many instances of a typeclass for a single type as you want, and you can provide the one that's more relevant to you. See where this is going? ;)

Typed dependencies in generic functions

Going back to the learnings from Part 1, we defined Syntax interfaces that were useful to describe the dependencies required by a function. The contrived example that we used was applied to a concrete interface RequestSyntax. Let's start with an example of a harmless library function:

fun <A> List<A>.remove(a: A): List<A> =
  this.filterNot { it == a }

This extfun can be part of a helper library and it works great when comparing elements with an obvious == identity, like DTOs. The problem arises when you're trying to remove duplicates from a list of Observable<A>, or () -> A.

val funcList = listOf({ 1 }, { 2 })

funcList.remove({ 1 })
// [{ 1 }, { 2 }]

This is because { 1 } == { 1 } is false as functions are compared by reference, and the value on each side of the operator is a different instance. This is also the case for most non-data classes.

So what we'd like now is to have a way of configuring this dependency without having explicitly having to pass a comparison function to every layer above it. Let's apply the technique of typed dependencies to build a syntax for Eq with a helper function to easily obtain it.

interface EqSyntax<F> {
  val eq: Eq<F>

  fun <A> A.eqv(a: A): List<A> =
    eq.eqv(this, a)
}

fun <F> Eq<F>.syntax() = object: EqSyntax<F> {
  override val eq = this 
}

And now we can refactor remove() to use the Syntax object:

object ListFunctions {
  fun <A> EqSyntax<A>.remove(l: List<A>, a: A): List<A> =
    l.filterNot { a.eqv(it) }
}

With this change we now have a remove function that can be called from any EqSyntax context and uses the Eq parameter to compare for equality. Let's use it:

val executeAndCompareEqSyntax = object: Eq<List<() -> Int>> {
 override fun eqv(a: () -> Int, b: () -> Int): Boolean =
   a() == b()
}.syntax()

executeAndCompareEqSyntax.remove(funcList, { 1 })
// [{ 2 }]

This is just but one possible implementation, you can go wild with them:

val isMultipleEqSyntax = object: Eq<List<() -> Int>> {
 override fun eqv(a: () -> Int, b: () -> Int): Boolean =
   a() % b() == 0
}.syntax()

val isNextEqSyntax = object: Eq<List<() -> Int>> {
 override fun eqv(a: () -> Int, b: () -> Int): Boolean =
   a() - b() == 1
}.syntax()

isMultipleEqSyntax.remove(funcList, { 5 })
// []
isNextEqSyntax.remove(funcList, { 1 })
// [{ 1 }]

Eq in this case acts like an implicit parameter for all calls to ListFunctions#remove(). This means that you'll be able to call remove from any deep call chain you can think of as long as it's within an implementation of EqSyntax.

Typeclasses and Syntax in Λrrow

UPDATE (Apr 14th '18): This article was written for Arrow 0.6. Starting on Arrow 0.7 all typeclasses are defined as the Syntax objects of this article!

When we defined the Syntax interface for Eq<F> before it could be easily spotted that it's all boilerplate code that can easily be autogenerated for us. Well, if you check the typeclass primer section above you'll see we annotated the interfaces with @typeclass. That will tell Λrrow's annotation processor to generate all necessary Syntax interfaces and syntax() extfuns in each Typeclass to easily create them.

Typeclasses 201

At this point I'd suggest you go and take a look at the full typeclass documentation in Λrrow to see the complete list of available typeclasses. You'll notice that many of them are parametrized on somethig called a Kind<F, A>.

A Kind<F, A> is an interface used as a way of representing any data or sealed class that has a generic parameter. This way a Option<A> implements Kind<ForOption, A>, or a Try<A> is generalised Kind<ForTry, A>. You can read more about these kinds in the glossay page of the documentation.

I'll list some cool typeclasses you'd like to look into:

  • Functor - its contents can be mapped

  • Applicative - allows independent execution, potentially paralellizable

  • Monad - allows sequential execution and starting coroutines

  • Monoid - combinable objects that have an empty value

  • Foldable - has a structure from which a value can be computed from visiting each element

  • Traverse - has a structure for which each element can be visited and get applied an effect

  • Async - can be created using an asynchronous callback function. It's also a Monad and an Applicative

An example in context

In this section I'll provide an example that uses the concepts seen to far to its full extent. Don't worry too much if you don't understand all the functional concepts presented on a first read, just follow the flow of the refactor to use Syntax interfaces and typeclasses :D

One piece of feedback I received for Part 1 was that the calls to network and databases looked synchronous in a way that, at least on Android, would block the UI thread and even cause a crash to desktop. This was intentional! Because we didn't use a typeclass that enabled code to be asynchronous we couldn't know whether those functions were written to be sync or async, and whether they were using a framework. Let's put Async to use!

Async is a typeclass that defines a new function async to enable asynchronous execution which provides a block to execute any code and must invoke a Callback to complete. It returns an asynchronous computation wrapped on an unspecified Kind container. This container will be concretized at the call site by an instance of Async for a class like Observable or Deferred.

The typeclass is defined like this:

typealias Callback = (Either<Throwable, A>) -> Unit

@typeclass(syntax = false)
interface Async<F> : MonadSuspend<F>, TC {
    fun <A> async(fa: (Callback) -> Unit): Kind<F, A>
}

Because this Callback is a simple function not opinionated about threads or frameworks it can be used to convert between implementations of asynchrony in any library. For example, if a function fetchTickets() was implemented in a library that enforces you to use Rx, you could wrap it in this agnostic callback like so:

fun fetchTickets(userId: UserId): Single<List<Ticket>> = ...

fun AsyncSyntax<F>.fetchTickets(userId: UserId): Kind<F, List<Ticket>> =
  async { cb: Callback ->
    fetchTickets(user)
      .subscribe(
        { value -> cb(Either.Right(value)) },
        { error -> cb(Either.Left(error)) })
  }

See that following this example we're now able to also refactor the API fetchUser() we created in Part 1 as asynchronous by modifying DtoSyntax and NetworkSyntax to depend on AsyncSyntax, and using functions like binding and async in their implementations.

We can now call fetchTickets or fetchUser for any implementation of Async like the ones provided by Λrrow, which includes effect frameworks like kotlinx.coroutines, Reactor (WIP), or even Rx2 itself. There's also IO, which is Λrrow's own abstraction.

At a small cost we can use Async instances to adapt asynchronous calls between frameworks, and use any framework-specific libraries with your favorite one.

You can also implement a new instance of Async for any framework, either internal or existing, and immediately start reaping these benefits. And with the power of Syntax, you can drill down the dependencies effortlessly:

DeferredK.async().syntax().fetchTickets("123").fix()
// Deferred<List<Ticket>>

ObservableK.async().syntax().fetchTickets("321").fix()
// Observable<List<Ticket>>

IO.async().syntax().fetchTickets("213").fix()
// IO<List<Ticket>>

You can also see another refactor I suggested on twitter this week!

Recap

In this blog entry we have focused into the feature that gives Typeclassless its name. We now understand how typeclasses are interfaces that define pure behavior without state. They are implemented outside of classes and are associated with them by their generic parameters instead.

Typeclasses are convenient and ubiquitous as configuration parameters, which makes them a perfect candidate to be passed around along large call chains using Syntax interfaces whithout putting a burden on a library user and implementer.

The implementer can code to any Typeclass abstraction that's available for any generic, including both content like User, Int or Company; and containers like List<A>, Observable<A>, Option<A> or Either<L, R>. Then, the user only has to create and provide the Syntax once at the origin of the call chain.

In Part 3 of the series I'll dig deeper on how this approach can be scaled for larger parts of your application by applying Syntax to classes, and some considerations to be taken into account.

Meanwhile, me and the other Λrrow contributors can be found in twitter, Gitter, or Github.