Does anyone have an example of how to write a pure...
# arrow
k
Does anyone have an example of how to write a pure function with logging with arrow? Would you use something like a writer monad?
r
Something along those lines. here we say there is an Algebra called
Logger
and has some
suspend
capabilities to log local or remote.
error
can't be called unless you are in a suspend context so in a way it's already protected for async and IO similar as to how you may encode this on a lang like monads such as Haskell.
IO a
here is represented as
suspend() -> A
with the same guarantees.
If you can use context receivers you can move the
Logger
to a dependency of your function.
Copy code
context(Logger)
suspend fun foo(): Unit =
  error("bar")
Nothing specific of arrow but Kotlin or Arrow does not bothered with Monads or transformers, instead it just uses suspension and the native support to inject context and abstract away implementations
d
Hi Raul, what is the benefit of
interface Logger
or `context(Logger)`` . Assuming that logging may be needed anywhere in the code than I will end up with one of: • all logic will be implemented as extension functions of
Logger
• all functions will require
context(Logger)
I’m asking because we base our code on functions as much as possible, but we have a “traditional” approach to logging -> we just use loggers where needed.
k
I am using context receivers for it. So wrapping any side-effect in suspend is the same as using IO in other FP languages?
d
Kristian, do you have logging context receivers defined for all/most methods/functions?
c
Another option that is less clean than context receivers is adding the Logger directly inside CoroutineContext. The benefit is that it doesn't impact the API, and that the caller can decide to enable or disable it. The downside is that it's all implicit, so it's very easy to forget to transmit it in
launch
.
k
@dnowak Really just in functions that do IO.
d
But it’s required in the whole call chain up to the function that does IO?
k
That depends on where the edge is defined and where you want to supply context
If you log everywhere everything will have it as a context I guess and you can just supply the context at application startup in your main function I guess. I'm just doing logging in my http handler functions and just model all my failures as sealed class hierarchy. So a failure doing some database operation will be returned up the call stack using context(Raise<Error>) and logged in the handler. This is my first project using context receivers, so I'm just figuring it out as I go though.
c
@CLOVIS something like this?
Copy code
interface Logger {
    fun error(msg: String)
}

data class CoroutineLogger(
    val logger: Logger
) : AbstractCoroutineContextElement(CoroutineLogger) {

    companion object Key : CoroutineContext.Key<CoroutineLogger>
}

fun CoroutineScope.log(msg: String) {
    coroutineContext[CoroutineLogger]?.logger?.error(msg)
}

suspend fun main() {
    val logger = object : Logger {
        override fun error(msg: String) {
            println(msg)
        }
    }
    val ctx = CoroutineScope(CoroutineLogger(logger))

    ctx.launch {
        log("Implicit logger")
    }

    delay(100)
}
r
@Kristian Nedrevold
I am using context receivers for it. So wrapping any side-effect in suspend is the same as using IO in other FP languages?
Yes, it's effectively the same to have a
IO<A>
as
suspend () -> A
both are functions that wrap a potentially async, blocking, cancelling... computation from being evaluated until you run your coroutines at the edge. In fact
suspend () -> A
is more general than
IO a
and it includes also
IO a
. It's more general because
suspend
models continuations that can fail with
Result<A>
which includes
Throwable
on the left, but suspend can also be seen as polymorphic and can be evaluated into any target
F[A]
. We see this in Arrow where
Effect
is folded into Either or whatever terminal value you want your suspended program. Unlike in those languages where IO is modeled as a data type in Kotlin
suspend
is modeled as a function type which you can use as a simple function modifiers. Suspend covers same laws and evaluation / cancellation semantics of IO. Continuations can model a lot more than just IO, they can model all monads https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/the-mother-of-all-monads If you squint they provide non blocking
F[A] => A
for all
F
. If you are aware of scala or haskell that operation is Comonad extract. Continuations embedded in Kotlin as suspend are dual and can model any monad bind operation
F[A] => A => F[B]
just as
F[A] => A
Continuations control flow and embedding in kotlin removes the need for
map
and
flatMap
in all boxed monads or types that emit a single value such as Option, Either and many others.
the proposed context receiver solution works not only on functions but also on classes.
Copy code
context(Logger)
class Repository
vs
Copy code
class Repository {
  val logger = Logger.getInstance
}
In which case all members of Repository can access the Logger without redeclaring contexts repeateadly.
If your functions are top level that duplication is good if you consider logging an effect that can alter the outcome of the function, otherwise println or similar hard impl may be enough.
k
@raulraja Thanks for the very detailed answer :)
133 Views