Typeclassless
techniqueArrow allows abstracting polymorphic code that operates over the evidence of having an instance of a typeclass available.
This enables programs that are not coupled to specific datatype implementations.
The technique for writing polymorphic code demonstrated below is available for all other typeclasses besides Functor
.
fun <F> multiplyBy2(FT: Functor<F>, fa: Kind<F, Int>): Kind<F, Int> =
/* ... */
multiplyBy2(Either.functor(), Either.right(1))
// Right(2)
multiplyBy2(Try.functor(), Try.just(1))
// Success(2)
In the example above, we’ve defined a function that can operate over any data type for which a Functor
instance is available.
And then we applied multiplyBy2
to two different datatypes for which Functor instances exist.
This technique applied to other Typeclasses allows users to describe entire programs in terms of behaviors typeclasses removing
dependencies to concrete data types and how they operate.
This technique does not enforce inheritance or any kind of subtyping relationship, and is frequently known as ad-hoc polymorphism
and frequently used in programming languages that support typeclass and typeclass.
Entire libraries and applications can be written without forcing consumers to use the lib author provided datatypes, but rather letting users provide their own typeclass instances for their datatypes.
Now, passing around a typeclass parameter like Functor
to every function is repetitive, tedious, and error-prone. For that reason, many programming languages provide a mechanism to describe the dependencies of the function without having to pass them as parameter explicitly. In Kotlin, this is achieved using extension functions.
As described in the glossary, typeclasses are a grouping of constructors and extension functions on Kind<F, A>
that form a DSL for the type they’re declared to. To access these extension functions, you need to be in the scope of a typeclass. That is, the typeclass object has to be bound to this
. Kotlin provides two ways of achieving this: With standard library functions, and with more extension functions.
The function multiplyBy2
, as defined above, receives FT: Functor<F>
as a parameter. We need to bind FT
to this
to be able to access the extension function fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>
inside. For that, we use the standard library function typeclass.run { }
, which requires a value return.
fun <F> multiplyBy2(FT: Functor<F>, fa: Kind<F, Int>): Kind<F, Int> =
FT.run { fa.map { it * 2 } }
If we have a block that does not return a value or Unit
, using either with(typeclass) { }
to emphasize the difference is recommended.
fun <F> printAllValues(S: Show<Kind<F, Int>>, fa: List<Kind<F, Int>>): Unit {
with (S) {
fa.forEach { println(fa.show()) }
}
}
An extension function is applied to a type that becomes bound to this
, and enables calling all its functions without using this.method()
. If we declare a function to depend on the typeclass, we get automatic access to the extension functions declared inside.
import arrow.*
import arrow.core.*
import arrow.typeclasses.*
fun <F> Functor<F>.multiplyBy2(fa: Kind<F, Int>): Kind<F, Int> =
fa.map { it * 2 }
And we can call it on the typeclass instances:
import arrow.core.extensions.*
import arrow.core.extensions.option.functor.functor
Option.functor().run {
multiplyBy2(Option(1))
}
// Option.Some(2)
import arrow.core.extensions.either.functor.functor
Either.functor<Throwable>().run {
multiplyBy2(Either.right(1))
}
// Either.Right(2)
The same applies to functions without a return value, so you don’t have to remember to use the right function.
fun <F> Show<Kind<F, Int>>.printAllValues(fa: List<Kind<F, Int>>): Unit {
fa.forEach { println(fa.show()) }
}
Knowing these two ways of accessing all the extension functions defined on a typeclass, how do we apply them to client code? This is heavily dependent on where Kotlin allows the use of extension functions. Let’s go through each case.
Any function that exists inside an object, companion object, or in the root of a file, can be declared as an extension function on its typeclass parameters. Extension functions declared for the same types can call into each other without passing the type parameter explicitly.
This is the simplest, most direct case, and that’s no coincidence. Most FP languages only allow functions scoped to global, a module, or an object that acts as a namespace. Programs are composed by functions and tested on their typeclass and data parameters that define the functionality.
fun <A> Eq<A>.remove(l: List<A>, a: A): List<A> =
l.filterNot { a.eqv(it) }
object FunctorLaws {
fun <F> Functor<F>.test(f: (Int) -> Kind<F, Int>): List<Law> =
listOf(
Law("Functor Laws: Covariant Identity", { covariantIdentity(f) }),
Law("Functor Laws: Covariant Composition", { covariantComposition(f) })
)
fun <F> Functor<F>.covariantIdentityTest(f: (Int) -> Kind<F, Int>): Unit = ...
fun <F> Functor<F>.covariantComposition(f: (Int) -> Kind<F, Int>): Unit = ...
}
...
import arrow.core.test.FunctorLaws.test
Option.functor.run {
test { it.some() }
}
fun <F> MonadError<F, Throwable>.ankMonadErrorInterpreter(): FunctionK<ForAnkOps, F> =
object : FunctionK<ForAnkOps, F> {
override fun <A> invoke(fa: Kind<ForAnkOps, A>): Kind<F, A> {
val op = fa.ev()
return when (op) {
is AnkOps.CreateTarget -> catch { createTargetImpl(op.source, op.target) }
is AnkOps.GetFileCandidates -> catch { getFileCandidatesImpl(op.target) }
is AnkOps.ReadFile -> catch { readFileImpl(op.source) }
is AnkOps.ParseMarkdown -> catch { parseMarkDownImpl(op.markdown) }
is AnkOps.ExtractCode -> catch { extractCodeImpl(op.source, op.tree) }
is AnkOps.CompileCode -> catch { compileCodeImpl(op.snippets, op.compilerArgs) }
is AnkOps.ReplaceAnkToLang -> catch { replaceAnkToLangImpl(op.compilationResults) }
is AnkOps.GenerateFiles -> catch { generateFilesImpl(op.candidates, op.newContents) }
}
}
}
As typeclasses are defined as interfaces in Kotlin, it’s trivial to convert any class you control into an implementer of a typeclass by using delegation.
data class Parser: Monad<ForOption> by Option.monad(), Traverse<ForListK> by ListK.traverse() {
fun parseInt(s: String): Option<Int> =
try {
parseOrThrow(s).just()
} catch (t: Throwable) {
none()
}
fun parseInts(l: List<Option<String>>): Option<List<Int>> =
l.k().traverse(this, { it.flatMap { parseInt(it) } }).fix()
}
val parser = Parser()
parser.parseInts(listOf("1".some(), "2".some()))
// Some([1, 2])
parser.parseInts(listOf("bla".some(), "2".some()))
// None
This is the case where we find the limitations of the Kotlin compiler. Once you’re inside a class, this
gets bound to the class scope.
This requires extension functions declared inside a class to use the standard library functions to access them, which is not very idiomatic.
// WRONG
class Parser {
fun Monad<ForOption>.parseInt(s: String): Option<Int> = ...
fun ???.parseInts(l: List<Option<String>>): Option<List<Int>> = ...
}
// TEDIOUS AND NOT IDIOMATIC
ForOption.run {
Parser().run { parseInt("123") }
}
For these cases, the most ergonomic option is to not use extension functions and, instead, fall back to regular parameter passing. Once inside the method, we can use the standard library functions to access the scope of the typeclass, and treat the method the same as if it was a scoped function.
class Parser {
fun parseInt(M: Monad<ForOption>, s: String): Option<Int> =
M.run { ... }
fun parseInts(M: Monad<ForOption>, l: List<Option<String>>): Option<List<Int>> =
M.run { ListK.traverse().run { ... } }
}
// JUST MEH
Parser().parseInts(Option.monad(), listOf("1".some(), "2".some()))
We are almost back to where we started with parameter passing, at least on this first layer. Except, there are some benefits once you’re inside these public functions, as you can declare private extension methods inside the class, and you will be able to seamlessly call them as well as other scoped functions.
In several of the prior examples, we have found that we depend on multiple typeclasses that are not part of the same hierarchy. This is not an uncommon case, and it has to be addressed. How can we express this combination? Luckily, the Kotlin language comes to our aid again.
Kotlin has a keyword where
, used in method signatures to describe multiple bounds on a generic parameter.
You can define multiple clauses for a single generic parameter that will be the type we declare an extension function on.
Let’s see one example of a function that depends on both Applicative
and Show
:
fun <F> Applicative<F>.findUserName(S: Show<User>, id: Kind<F, UserId>): Kind<F, String> = S.run {
id.map { fetchUser(it) }.map { it.show() }
}
Let’s do a rewrite using where
to define multiple typeclass requirements.
We will create a generic parameter TC
that is bound to both typeclasses. That parameter will be the one we declare an extension on.
We’ll also remove running Show
, as it isn’t required anymore.
fun <F, TC> TC.findUserName(id: Kind<F, UserId>)
: Kind<F, String> where TC: Applicative<F>, TC: Show<User> =
id.map { fetchUser(it) }.map { it.show() }
Nice! Problem solved! Now, how does one create an Applicative
+ Show
?
At this point is where type inference gives us an easy way of creating objects that define bounds for multiple typeclasses. A simple helper function with an inferred return parameter saves us from having to write the type manually:
fun <F, T> createApplicativeShow(AP: Applicative<F>, S: Show<T>) =
object: Applicative<F> by AP, Show<T> by S
createApplicativeShow(Option.applicative(), Show.any())
.findUserName(Some("1"))
// Some("Paco")
createApplicativeShow(Either.applicative<Throwable>(), Show.any())
.findUserName(Some("1"))
// Right("Paco")
Alternatively, you can compose the types in a new interface and give it a new name.
This way, you can also skip where
clauses.
The major downside is that you’ll find a combinatory explosion of new interfaces on your codebase,
so use them scarcely when you have a type that repeats itself pervasively through the codebase.
interface ApplicativeShow<F, T>: Applicative<F>, Show<T>
fun <F> ApplicativeShow<F, User>.findUserName(id: Kind<F, UserId>): Kind<F, String> = ...
fun <F, T> createApplicativeShow(AP: Applicative<F>, S: Show<T>): ApplicativeShow<F, T> =
object: ApplicativeShow<F, T>, Applicative<F> by AP, Show<T> by S
Another reason to create a new real type is when you have two colliding typeclass instances.
You may find some cases where two typeclass requirements collide because they’re defined for the same type, or include a typeclass on the same hierarchy.
Another way of looking at this kind of collisions is when annotation-based DI frameworks need javax.inject.Named
.
In the case of Arrow, you will need to disambiguate them manually based off fields. This has some benefits over the DI framework approach:
This collision happens in an example we saw before. The typeclasses Monad<F>
and Traverse<F>
both inherit from Functor<F>
.
You cannot define a boundary for two different values of F
. This happened in our Parser class:
// Compiler error: Type parameter <F> for 'Functor' has inconsistent bounds: ForOption, ForListK
data class Parser: Monad<ForOption> by Option.monad(), Traverse<ForListK> by ListK.traverse()
As with methods inside a class, we have a situation where multiple this
bindings collide.
The way of disambiguating is by creating a new type that provides both bounds as functions, and has an instance in its companion object:
interface ParserDependencies {
fun MO(): Monad<ForOption> = Option.monad()
fun TL(): Traverse<ForListK> = ListK.traverse()
companion object: ParserDependencies
}
And now we can rewrite our parser class avoiding ambiguity:
data class Parser: ParserDependencies by ParserDependencies {
fun parseInt(s: String): Option<Int> = MO().run {
try {
parseOrThrow(s).just()
} catch (t: Throwable) {
none()
}
}
fun parseInts(l: List<Option<String>>): Option<List<Int>> = TL().run {
l.k().traverse(this, { MO().run { it.flatMap { parseInt(it) } } }).fix()
}
}
We have the same benefits and pitfalls we had in methods inside a class.
Once inside the run
blocks, we’re able to call all other scoped extension functions declared for the enclosing typeclass.
This disambiguation only happens in one of the layers, which is responsible for the ambiguity in the first place!
Cool. Once we have all the pieces in place, explained, and understood, let’s mention one last use case: General dependency injection!
Now that you know how to define new types to declare multiple dependencies, and add fields to prevent collision between typeclasses in the same hierarchy, what prevents you from adding arbitrary data to any fields of the type? Well, the answer is nothing. :D
Let’s grab one of our previous examples and refactor it
class UserFetcher<F>(ME: MonadThrow<F>, val api: ApiService): MonadThrow<F> by ME {
fun getUserFriends(fid: Kind<F, UserId>): Kind<F, List<User>> =
fx.monadThrow {
val (id) = fid
val (user) = api.getUser(id)
user.friendIds.map { api.getUser(it.id) }.bind()
}.handleError { listOf() }
fun createId(id: String): Kind<F, UserId> = ...
}
On the first step, we’ll create a new type enclosing all the dependencies:
interface FetcherDependencies<F>: MonadThrow<F> {
fun api(): ApiService
companion object {
operator fun invoke(ME: MonadThrow<F>, api: ApiService): FetcherDependencies =
object: FetcherDependencies, MonadThrow<F> by ME {
override fun api() = api
}
}
}
With this type, we don’t need a class anymore, as it already encloses all state.
We can move the functions to an object scope, and make them depend on FetcherDependencies
and MonadError<F, Throwable>
.
object Api {
fun FetcherDependencies.getUserFriends(fid: Kind<F, UserId>): Kind<F, List<User>> =
fx.monadThrow {
val (id) = fid
val (user) = api().getUser(id)
user.friendIds.map { api().getUser(it.id) }()
}.handleError { listOf() }
fun MonadThrow<F>.createId(id: String): Kind<F, UserId> = ...
}
...
import Api.getUserFriends
val deps = FetcherDependencies(Either.monadThrow(), ApiService())
deps.getUserFriends(deps.createId("1234"))
.fix().unsafeRunSync()
// [User(id="33", friendIds=["1234", "987"])]
What have we gained from this change? We have replaced one concretion, a final class, with an abstraction that’s an interface.
And as a consequence to all of this, testability and refactoring possibilities are through the roof! Many of the promises of OOP, fulfilled with simple functions and interfaces.
import Api.*
with (Option.monadError()) {
createId("123") shouldBe Some(123)
createId("-123") shouldBe None
createId("asfgasf") shouldBe None
createId("") shouldBe None
}
with (FetcherDependencies(Option.monadError(), MockApiService())) {
getUserFriends(createId("")) shouldBe listOf()
getUserFriends(createId("sfgafg")) shouldBe listOf()
getUserFriends(createId("123")) shouldBe listOf(User("321", listOf("123")))
}
Typeclasses are completely stateless collections of functions. Once you start adding other objects that are stateful, you need to take care of the lifecycle of your Dependencies object.
Luckily for us, because there is only one reference to the Syntax object that’s passed around across layers through function parameters instead of being retained by classes, it’s easy to track its lifecycle and manage it.
Assuming this Dependencies object is completely stateless and it lives at the global scope, a simple root value suffices:
import Api.*
val deps = FetcherDependencies(Either.monadError(), ApiService())
fun main(args: Array<String>) {
println(deps.getUserFriends(deps.createId("1234")))
}
Let’s introduce some nuance and assume the scope is now per-screen, as you would with Android activities. The expected lifecycle of the Dependencies object is the same as the Activity, and gets garbage collected alongside it.
import Api.*
class SettingsActivity: Activity {
val deps = FetcherDependencies(Either.monadError(), ActivityApiService(this))
override fun onResume() {
val id = deps.createId("1234")
user.text =
id.fix().map { it.toString() }.getOrElse { "" }
friends.text =
deps.getUserFriends(id).fix().getOrElse { emptyList() }.joinToString()
}
}
To recap, to scope efficiently means that you have to manually create and store your Dependencies object once, at the origin of the scope, and can now implicitly pass it around across all other layers without explicitly using it as a parameter. Each function is also limited to only use the dependencies that are relevant to itself.
Do you like Arrow?
✖