MIDI
02/06/2025, 4:17 PMYoussef Shoaib [MOD]
02/06/2025, 4:33 PMEither<E, Writer<X, A>>
keeps logs only upon success. A Writer<X, Either<E, A>>
keeps logs in both cases.
Passing an accumulate
function along to add the errors is definitely the Kotlin idiomatic approach to do this. This is analogous to how Raise passes a raise
function around. This technique is more broadly called "capability passing style", where you pass along a way of doing some "effect". This is exactly what Reader is. In fact, this technique basically allows any monad to be viewed as Reader and Cont (if interested, look into the concept of delimited continuations and algebraic effects).
Let's get back to Kotlin:
fun interface Write<S> {
fun write(s: S)
}
inline fun <S, R> writer(block: Write<S>.() -> R): Pair<List<S>, R> {
val result: R
return buildList {
result = block({ add(it) })
} to result
}
// Usage
writer {
either {
write("foo")
raise(42)
}
} // == listOf("foo") to Left(42)
either {
writer {
write("foo")
raise(42)
}
} // == Left(42)
The nice thing is that you can write a function that uses Raise
and Writer
without caring about what order they're nested in. Then, the caller decides the nesting based on what they want, simply by changing the nesting of the "handlers" (here either
and writer
)
I'll leave it as an exercise to you to change this to work with monoids (hint: fold
).Youssef Shoaib [MOD]
02/06/2025, 4:35 PMYoussef Shoaib [MOD]
02/06/2025, 4:37 PMYoussef Shoaib [MOD]
02/06/2025, 4:47 PMMIDI
02/07/2025, 8:55 AMEither
). So at the moment, I'm trying to grasp the concepts without Arrow to learn their inner workings before returning to Arrow and seeing how it can help me save some boilerplate. ๐ And at the same time, I get to practice Kotlin a bit more - I've only written Kotlin for about 1,5 years.
I will take some time to digest your answer and your examples, but a few questions popped up that I can ask right away! You mention the approach of using Writer<X, Either<E, A>>
to log "in both cases". Does that mean I would use Either
to carry unexpected Exceptions as well as "domain failures"? Last year I dove into error handling in Kotlin and tried my best to interpret what the recommended Kotlin-idiomatic way is. After learning about Result
and its shortcomings, I formed a picture where I would use Either
-like structures for domain failures and top-level try-catch blocks (or `CoroutineExceptionHandler`s) for unexpected Exceptions.
A related question: A "handler for Write that is aware of your logging framework, and it'll will simply log the strings when an exception happens" sounded interesting! One aspect my brain is still getting used to is imagining "where" things get called. (I'm still kind of new to coding, graduated 2022 ๐ ) My "exercise project" is a system at work where I'm trying to rewrite the "domain" subproject in an FP style, and my mental model so far has been that no IO calls should be made "in" the domain, so to speak. The Writer with a handler that calls my logging framework, would that include a try-catch that invokes the handler and then re-throws the exception? Would that mean that my mental model gets broken as an IO call is made "in" the domain? Or is this one of those things where I need to shift my thinking to "all of the operations and calls defined in the use case in the domain are not made there, they are simply an instruction on how things should be done, and it is the caller that makes it all happen when it invokes the use case, so that IO logging call is not made in the domain - it is just defined there"? Sorry for the very philosophical question!! ๐
...oh, reading your suggestion one more time after writing all of that made me think that you maybe meant that the caller could pass in an "implementation" of a logging function (containing the actual call to the logging framework) to the Writer that should be invoked on catching-and-re-throwing exceptions - and then things would still be IO-free in the domain? ๐Youssef Shoaib [MOD]
02/08/2025, 6:14 AMResult<R>
, where Result<R> == Either<Throwable, R>
. So back to your question. You'll lose logs if your program is ultimately Result<Writer<S, R>>
, but you won't if your program is Writer<S, Result<R>>
. Hence you need to have your Write
handler somewhere near the root of the program. What you suggest in your last paragraph is absolutely right: you can pass in your logging implementation still, and basically have an intermediate handler that keeps local logs, but pushes them upstream when an exception happens.MIDI
02/10/2025, 3:43 PMEither
-style Monad even for Exceptions? So you catch all Throwable
(that would include Error
, so perhaps just Exception
?) right where they arise and wrap them in an Either
? Should "domain failures" be right-side then, like Either<Throwable, DomainResult>
?MIDI
02/17/2025, 3:51 PM