3

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?

Ulrich Schuster
  • 1,670
  • 15
  • 24

1 Answers1

5

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
}
mkobit
  • 43,979
  • 12
  • 156
  • 150
nomisRev
  • 1,881
  • 8
  • 15
  • Thanks for your detailed answer. I am still stuck though - with the solution via extension functions, I would still need to pass my config object (an instance of DataModule in this case) to the place where I actually call it, which might be several layers deep in a call stack. I don't see how the solution here would help me to thread the environment through the call stack without explicit argument passing. – Ulrich Schuster Oct 19 '22 at 11:17
  • I added an implementation for Reader that supports `suspend`, and a DSL similar to Arrow. I hope that helps @UlrichSchuster. I left the remainder of the answer as is, since context receiver will be the future for this pattern in Kotlin. – nomisRev Oct 19 '22 at 14:33