I'm looking a bit more at what might be possible n...
# arrow-contributors
s
I'm looking a bit more at what might be possible next for the Ior and error accumulation APIs after adding that
accumulate()
function to
IorRaise
a while back. I'm wondering if now it would make sense to have variants of
mapOrAccumulate
and other related functions that operate on an
IorRaise
along side the ones that operate on
Raise
. When used in the context of an
IorRaise
their behaviour would be to accumulate errors and mapped items into a
Both
case, like so:
Copy code
val nums = listOf(1, 2, 3, 4, 5, 6)
val func: RaiseAccumulate<String>.(Int) -> Int = {
  if (it > 4) raise("$it") else it * 10
}

either<String, Unit> {
  val mapped = iorNel {
    mapOrAccumulate(nums, func)
  }

  val eitherMapped = nums.mapOrAccumulate { func(it) }

  println(mapped) // prints Ior.Both(NonEmptyList(5, 6), [10, 20, 30, 40])
  println(eitherMapped) // prints Either.Left(NonEmptyList(5, 6))
}
Is this something that has been considered at all? Is it desirable, or would it cause unintended issues or confusion? My main train of thought with this is working predominantly inside normal
Raise
scopes inside of an
either {}
DSL, but wanting to encapsulate a small part of my logic in an
IorRaise
scope using a nested
iorNel {}
, in which I can run something like
mapOrAccumulate
to map a list of data, collecting both the errors and the successfully mapped items.
s
Very interesting! 🤔
Is this something that has been considered at all? Is it desirable, or would it cause unintended issues or confusion?
The idea has been there, but it was never explored. At this point I'm not sure what any unintended issues could be. This was one of the ideas I had for
Raise
. Afaik you should even be able to refactor
func
to just
Raise
and it should still work.
👍 1
s
I did a quick proof of concept with just this function to see that it works. When I get a moment I'll make a pass on the rest of the RaiseAccumulate file and create versions of the functions that operate on IorRaise
🙏 1
Good point on only needing the Raise receiver on the mapping function. That's what I quite liked about this approach. That you can keep using raise as normal within the mapping function but outside you can get a different result depending on the context in which you use it
💯 1
❤️ 1
s
Awesome, cool thank you!
s
I've done a straight up copy of all of the `mapOrAccumulate`/`forEachAccumulating` functions in
RaiseAccumulate
and swapped the
Raise
receiver for
IorRaise
. There's some interesting challenges here and there which will definitely need context parameters to solve nicely. As an example, I can't simply duplicate the
mapOrAccumulate
function that takes an
Iterable
receiver and returns an
Either
, it would need to be moved into the
Raise
class, and a duplicate would have to go into the
IorRaise
class. Similarly, when using
mapOrAccumulate
from inside an
ior(combineError = ...) { ... }
builder, the combine function can't be accessed from the extension, so to make a version of
mapOrAccumulate
work without a combine parameter, it has to be moved into the
IorRaise
class directly. I've ended up with this quick example case, building upon my earlier one which seems to flow nicely in the different use cases. But there is potential for some confusion. This may just be because I was still filling out functions, but before I realised that
combineError
wasn't being inferred, I had exactly the same code as this which all compiled, but the
mapOrAccumulate
in the
ior(...) {}
builder was actually implicitly falling through to the parent
either {}
builder because there was no matching overload in
IorRaise<Error>
. It's resolved simply by making sure the overload exists on
IorRaise
for that case (by creating one directly in the class as mentioned above). But it does show an area where if all cases aren't covered, unexpected behaviour will occur.