is there a folding equivalent to traverse? so if i...
# arrow
t
is there a folding equivalent to traverse? so if i have a list of functions returning
ValidatedNel<A, B>
i can thread an argument through them, returning
ValidatedNel<A, B>
where
B
is the return value from the final function in the list if all succeed, otherwise the errors? feels like i should be able to do this with
foldMap()
...
a
you should be able to accomplish this with a combination of
mapOrAccumulate
and
bind
Copy code
mapOrAccumulate(listOfFunctions) { fn ->
  fn(argument).bind()
}.bind().last()
• the
mapOrAccumulate
will iterate through the list and accumulate the errors • the
bind()
inside the block is necessary to “embed” the
Validated
result into
Raise
• the outer
bind()
to short-circuit in case the
mapOrAccumulate
returns a
Invalid
• and then you use
last()
to get the last element
t
makes sense, except i need to pass the return from
fn()
into the next one, not call each one with
argument
.
y
Oh I think you can just do:
Copy code
listOfFunctions.fold(firstArg) { arg, fn -> fn(arg).bind() }
While making sure you're inside a
Raise<Nel<A>>
And if you make your functions
Raise<Nel<A>>.(B) -> B
instead of
(B) -> ValidatedNel<A, B>
(which are semantically equivalent) then you can remove the bind as well. Conceptually, your functions are
(B) -> B
but with the possibility of raising a
Nel<A>
. Once that's your mental framework, it's easy to see that
fold
is the right option to transform a
List<(B) -> B>
and a first argument
B
into a return value of
B
, and handling the
Nel<A>
becomes merely an implementation detail, yet your functions are still referentially transparent and pure.
t
awesome! some of this is new in latest arrow i believe? fewer implementation details 😉
y
Yes, although it was experimented with before previously with Fx blocks, Continuations, Effect Scopes, and now (the best version) Raise. Raise is awesome because it's just purely based on exceptions, and so other libraries are very used to being "hands off" with them, rethrowing any exceptions that user-code generates. While exceptions are generally "impure", Raise is written in such a way that near-guarantees referential transparency (I.e. "it does what it says on the tin"). It can only run into trouble when dealing with libraries that swallow exceptions in some way (which is quite frowned upon in JVM land already), and when that happens, you'll have a nice clear message in your console telling you that the raise instance was leaked or swallowed somehow, with a stack trace and everything, thus pointing you towards the offending code. It basically allows non-local returns. Wrapping your code in one of the Raise builders just allows you to handle the "successful" (normal value returned from the block) and the "failure" (non-local value returned from somewhere in the code, usually representing an error, but doesn't have to be an error at all). It's (IMO) the most idiomatic system for error-handling that you can have in Kotlin because it leverages the language's affinity for inlining and lambdas, and hence it works seamlessly, as if it was a language feature. The caller is hence forced to either handle the error somehow, or pass the requirement of handling the error onto its callers (with context receivers). If you really really want, you can also create an "unsafe" raise builder that just throws exceptions on error, and you'd just wrap the code that needs Raise in this new "unsafe" block. It's obviously not recommended, but that's the thing, because Raise is just like any other type, you can just define your own functions that have custom error-handling strategies, or simply just pass the requirement of error handling up the call chain. All of that, and it really doesn't lock you into a specific data type. Again, Raise just declared the possibility of a non-local return. It's a general and fundamental concept, but it doesn't interfere at all with your functions and their signatures (except by adding an extra context receiver). This is unlike the other "functional" approaches of wrapping monads within monads, until you end up with a massive incomprehensible mess, with the value that you want existing within many, many layers of explicit nesting. Raise provides a less-mentally demanding form of that by simply reminding you that you have to handle errors at some point, but without your code suffering from a constant need to differentiate when calling code that can throw errors and code that can't. As the amount of Raise-based functions in Arrow grows, you'll see some very natural patterns being represented concisely in Kotlin with unimaginable performance. Raise is well-and-truly optimized for the normal return path (because it works just like any other function that can throw exceptions). TL;DR: Raise is awesome :D
a
thanks for the great and extensive answer, @Youssef Shoaib [MOD]! I just want to add a link to the docs, in case you want to see more examples about this accumulation https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#accumulating-errors