I working on a medium-sized Kotlin project where I need to thread configuration information read from file through many nested calls of pure functions. This seems to be an obvious case for the Reader monad. However, I have not figured out how to effectively implement Reader in Kotlin.
I am using the Arrow library (v1.1.3), but - to my surprise - it does not come with an implementation of Reader. What is the preferred way to thread configuration data through function calls with Arrow? As Arrow has shifted to using Kotlin's native suspend system for monad comprehension, I take this to mean that there is no need to have a dedicated Reader implementation. How to do it instead?
Arrow used to have the reader monad in the past, but we've since then stopped supporting such wrappers in favour of Kotlin idiomatic patterns.
There are serveral ways you can solve this in Kotlin, but the most promising is context receivers
.
A concrete example can be found here, and a small video tutorial here.
This however is not yet stable in Kotlin, and only available for the JVM for now. There is a way to solve the same issue using extension functions but it currently requires a bit more boilerplate. Where you extend a generic type of R
(Reader
), and you constraint R
to the instances you require.
suspend fun <R> R.getProcessUsers(/* add any arguments as needed */): Either<ProcessingError, List<ProcessedUser>>
where R : Repo,
R : Persistence =
fetchUsers().process()
To then finally call this function you need to make R
concrete, you do this by making Repo
and Persistence
and interface and then you can use delegation.
class DataModule(
persistence: Persistence,
repo: Repo
) : Persistence by persistence, Repo by repo
suspend fun main(): Unit {
// This is your router { get { } } router definition or
// your Android launch { } or compose function.
// Generic top-level function automatically got enabled
val processedUsers = DataModule(MockPersistence(), MockRepo()).getProcessUsers()
println(processedUsers)
// Call the alternative approach
val processedUsers2 = DataModule2(MockPersistence(), MockRepo()).getProcessUsers2()
println(processedUsers2)
}
It's possible to however still implement Reader
, but it should probably be a ReaderT
variant which implements a suspend
version of it.
EDIT:
An implementation of suspend
supported Reader
with DSL similar to Arrow can be implemented like this:
public class Reader<R, A>(public val reader: suspend (R) -> A) {
public companion object {
public fun <R> ask(): Reader<R, R> = Reader { it }
}
public fun <T> local(f: (T) -> R): Reader<T, A> = Reader { r: T -> reader(f(r)) }
}
public interface ReaderEffect<R> {
public suspend fun <A> Reader<R, A>.bind(): A
}
public fun <R, A> reader(action: suspend ReaderEffect<R>.(R) -> A): Reader<R, A> =
Reader { r ->
val effect = object : ReaderEffect<R> {
override suspend fun <A> Reader<R, A>.bind(): A = reader(r)
}
action(effect, r)
}
public val one: Reader<String, Int> = reader { input -> input.toInt() }
public val sum: Reader<String, Int> = reader { one.bind() + one.bind() }
public suspend fun main(): Unit {
val res = sum.reader.invoke("1")
println(res) // 2
}