What would be the best approach for modelling a fu...
# arrow
k
What would be the best approach for modelling a function that needs to accumulate errors, accumulate warnings, and possibly return a successful return object. From the docs and experience, something like an
IorNel<Error, R>
where errors are accumulated would work. However, where would one model the warnings? Sub type of the error type and then sift through the accumulated errors to see if’s tagged with a warning interface? Or wrap the return result in some
Warning
monad?
The goal is to process a bunch of related things, accumulate errors when applicable, and also accumulate warnings where warnings are applicable plus return a result if successful
c
When a warning is emitted, can the process still be succesful?
k
Correct, warnings aren’t technically errors in this case
c
If the process fails, do you care about warnings that were emitted before failure?
k
Negative, I still need the warnings
c
IMO, your best bet is
EitherNel<Warning, EitherNel<Error, Success>>
. Verbose, but you do want a complex thing…
Wait no
Pair<Nel<Warning>, EitherNel<Error, Success>>
is closer to what you want
It'd probably be easier to create a new type algogether
Copy code
sealed class OutcomeWithWarnings<out W, out F, out S> {
    abstract val warnings: List<W>
}

data class WarningsOnly<W>(
    override val warnings: Nel<W>
) : OutcomeWithWarnings<W, Nothing, Nothing>()

data class Failed<W, F>(
    val errors: Nel<F>,
    override val warnings: List<W>
) : OutcomeWithWarnings<W, F, Nothing>()

data class Success<W, S>(
    val value: S,
    override val warnings: List<W>
) : OutcomeWithWarnings<W, Nothing, S>()
but you'd have to re-create the entire DSL
k
I’m leaning towards recreating the DSL using a tri-type
c
It's not really a tri-type, because you can have warnings in all three cases
It's much more similar to my
ProgressiveOutcome
but with warnings instead of progress information: https://gitlab.com/opensavvy/groundwork/pedestal/-/blob/main/state/src/commonMain/kotlin/progressive/Progressive.kt?ref_type=heads#L30
👍 2
👍🏾 1
d
It seems like I need something similar... I'm trying to accumulate logs for steps that were completed in a process, and should be returned in a happy path, but also in the Left it should be there along with the error... how would I use raise or return the logs in this case? I'm not too sure I understand what's going on in that outcome class. Maybe an Ior.Both does fit here? Or is there something planned to fill this seemingly common use case officially in Arrow?
a
so you want to return the logs regardless of it completing successfully or with error?
d
Yes
A list of log lines
accumulated when passing each step
a
then your logs are not really tied to the failure or success; I recommend using something along the lines of
Copy code
data class Result(
  val warning: MutableList<Warning>,
  val result: Either<Error, Result>
)
d
How would I use that with
either { }
and
raise
?
(Btw In this case there's no
Result
... so that would be
Unit
)
a
maybe you can do something along the lines of?
Copy code
fun Raise<Problem>.foo(
  warnings: MutableList<Warning>,
): Unit
d
The step encompass a few UseCases though... and each needs to be binded to the main UseCase, where all the lines are accumulated to be stored in a db field
Each UseCase is something like:
Copy code
fun interface UseCase1 {
   operator suspend fun invoke(...): Either<..., ...>
}
a
I'm not 100% sure how that would look, but it feels to me that your accumulation of warnings is independent of the Raise part
d
And the main one has one big recover block binding them all together and saving to the db field on success or failure. You're right, but when
raise
is called, I'd still need a way to pass what I have so far of logs... so I can report them in the field in the parent use case.
Otherwise, I can't just
return
the logs, since
raise
will just jump to the error lambda of recover and skip that return...
So in a sense, it IS linked to Raise, no?
k
A naive implementation could look something like
Copy code
interface WarningCollector {
  fun get(): List<String>
  fun addWarning(warning: String)
}

val collector = object : WarningCollector {
  private val mutableList = mutableListOf<String>()
  override fun get(): List<String> = mutableList
  override fun addWarning(warning: String) {
    mutableList.add(warning)
  }
}

context(WarningCollector)
fun foo(): Either<Error, String> = either {
  addWarning("foo returning foo")
 "foo"
}

context(WarningCollector)
fun bar(): Either<Error, Int> = either {
  addWarning("bar returning 1")
  1
}

context(WarningCollector)
fun baz(): Either<Error, Boolean> = either {
  addWarning("things go boom")
  raise(Error("boom!"))
}

fun main() {
  with(collector) {
    foo()
    baz()
    bar()
  }
  collector.get().forEach(::println)
}
d
Yeah, that's with context receivers... which I don't have in this project 😞...
That's probably much cleaner, and separates the responsibilities... but in my case...
(or in any case anybody's using Either instead of Raise............)
I guess I'll have to pass in the collector as a parameter to my invoke... still not as nice (even the context receiver has this problem---) since I'm passing a mutable thing around instead of staying immutable globally (locally mutable isn't as bad AFAIK...)