New to Arrow, I'm experimenting using it to replace Exceptions. Nothing async. I've a few functions ...
n
New to Arrow, I'm experimenting using it to replace Exceptions. Nothing async. I've a few functions that return Either<> that are called in series by a function that also returns either. If one of the called functions returns Left is there a neat way for the parent to immediately abort and return that Left?
a
I gave a talk recently on better error handling with Arrow. Check out how neatly you can call a chain of functions that return an Either: https://github.com/adomokos/kotlin-sandbox/blob/master/src/main/kotlin/sandbox/github/explorer/EitherApp.kt#L64-L68
If the
deserializeData
returns
Left
the call chain will return that
Left
value.
n
Thanks. That's neat if everything is chained using
.map{}
, but that adds quite a lot of boilerplate around each function call.
a
Well, context free function is invoked with
map
, monadic functions are invoked with
flatMap
.
chaining dependant operation should use flatMap not map
b
ā˜ļø
and the boilerplate will be greatly reduced when using the comprehension syntax
n
Ok. Still though you need that boilerplate around each function call.
a
If you look at the example I showed you, the individual functions are returning
Either
values, or just operate on functions without the
Either
context. I am not sure that is more boilerplate than doing
try/catch
calls in functions.
b
E.g. something like the following is actually a chain of
.flatMap
in the
fx
block
Copy code
fun f1(...): Either<Throwable, Int> = ...
fun f2(x: Int): Either<Throwable, String> = ...
fun f3(y: String): Either<Throwable, Boolean> = ...

Either.fx<Throwable, Boolean> {
    val (a) = f1(...)
    val (b) = f2(a)
    val (c) = f3(b)
    c
}
j
that is equal to
f1().flatMap(f2).flatMap(f3)
isn't it?
fx
doesn't always make code cleaner
ā˜ļø 1
b
Yes. Depends on the situation, I think
n
@Attila Domokos same here, each function returns an
Either
. So I need to chain them and individually wrap each one in
flatMap()
or
map()
as in your example to have the later ones abort if the first returns Left. But when throwing an Exception I would just have one try/catch block wrapping the whole series.
b
Normally exception handling is handled by
IO
, not
Either
and with
IO
the boilerplate is still overall less than a top-level `try`/`catch`
plus nifty combinators like
attempt
and
handleErrorWith
are available to deal with error conditions
also resource safety with
Bracket
and
Resource
in particular,
Bracket
with `try`/`catch` only is very difficult to write
n
@Bob Glamm thanks, so
fx{}
aborts if anything returns a
Left
? That would do what I want... but it looks like I need to assign a var to the return of every function to trigger that?
j
Handling exceptions with
Either
is fine. Catching them is not something
Either
should be used to do.
IO
is much nicer in that regard because you can define handlers at very specific levels, it automatically catches exceptions and is pure code.
b
Yes, the
val (...) = expr
syntax de-sugars to repeated
flatMap
I think for your specific code Jannis is correct,
f1.flatMap(f2).flatMap(f3)
is cleaner
n
ah ok. Some functions just return Unit on the Right, so I'm doing a wasted assign.
b
Use
!
or
.bind()
n
but writing
flatmap()
lots of times is quite verbose/boilerplate-y for sync code.
b
e.g. if
f2
above returned
Unit
then
!f2(a)
instead of
val (b) = f2(a)
if it's lots of times then it's more likely to use
fx
comprehensions
in my experience the value of types with
flatMap
far outweighs the (currently) somewhat unusual syntax
but (IMO) that becomes more obvious when using
IO
a
flatMap
can be noisy when your coming from Haskell, but like @Bob Glamm mentions it, there is quite a bit of functionality you can rely on.
n
I'm coming from Java and finding it much noisier than Exceptions šŸ™‚
j
It's a cleaner pattern though. It makes exception paths explicit. Though if you end up wrapping literally everything in
Either
you kind of loose the benefits^^ Also using
Throwable
in left is kind of sub-par. A more explicit type might be better
n
@Bob Glamm ah sorry the function doesn't return Unit, it returns
Either<MyDomainError, Unit>
. Compiler likes
val ignoreme = f2(a)
but not
!f2(a)
Or should I look at
IO
instead?
b
Could be
f2(a).bind()
instead of
!f2(a)
. Sorry, it's been a couple of months since I've used Arrow
j
You cannot mix
Either<L1, A>
and
Either<L2>
btw,
Either
only composes if the left is the same.
b
I tend to just use
IO
for operations that yield traditional side-effects: e.g. opening/reading from a file, read/write database, HTTP endpoints, etc.
ā˜ļø 1
I would use
Either
in cases like signature verification or decryption - things that can fail but aren't necessarily a traditional side-effect
(although in the latter case I get lazy and just use MonadError anyway)
n
Yes the Errors are my own domain model hierarchy, not extending Exception.
j
(although in the latter case I get lazy and just use MonadError anyway)
With polymorphic
F
or of a fixed type? Because that usually leads to mtl style later? o.O
b
polymorphic F
j
bind
is only available inside
fx
and only for the correct `L`:
Either.fx<L, R>
only provides
Either<L, *>.bind()
n
Yes, from reading
IO
it does seem to be about async things and at the moment everything in our codebase is very much sync.
b
IO handles sync and async
Let me see if I can find the relevant typeclass diagram
n
ah, ! works, interesting, will have to find what that is...
b
sadface, the typeclass diagrams are missing from arrow-kt.io šŸ˜ž
Anyway, traditionally
IO
has a number of typeclass instances, so it handles Async, Sync, Monad*, Applicative*, etc.
j
IO
in arrow has a few purposes: • It defers computation (and thus makes it lazy and pure) Good for referential transparenty and thus easy refractoring • It provides powerful methods of handling errors/exceptions with resource safety • It provides easy methods to run code concurrently (and keeping the guarantees from errors handling over async bounds) It is generally a good idea to use it if your program involves side-effects and you may want to go async at some point. For error handling stick to what @Bob Glamm referred to above:
IO
for code you don't own and that involves side-effects (like throwing) and a different exception type for error paths in pure code (like
Option
,
Either
)
@Bob Glamm sync is a scala cats thing afaik. We named it
MonadDefer
for some reason^^
b
So if your functions are synchronous:
Copy code
def f1(...): Kind<ForIO, Int> = ...
def f2(...): Kind<ForIO, String> = ...

IO.fx { 
  val (a) = f1(...)
  val (b) = f2(a)
  b
}
@Jannis ahh. I've been bouncing between Arrow & Cats in order to try to expand my understanding
I'm currently in Cats land
n
Is there a way to build the
.bind()
into
f2()
so that the caller doesn't have to assign to an unused var or
!
or
.bind()
?
b
Building it into f2() means evaluating the effect eagerly instead of lazily
Think of it this way: using an
fx
block is constructing a program that will be executed later within the given effect (Either, IO)
The functions will not be called until you run the program, e.g. with
.fold
(for Either) or something like
.unsafeRunSync
(for IO)
That's intentional so that effects occur at the program boundary. Everything else is referentially transparent so you can (for instance) pass that program around and use it as freely as you'd like
j
It is not a problem to "build" it in as long as it is in an
fx
block. But that is hard to do in arrow atm
n
this is all sync code, it will all be run immediately anyway.
hmm I guess f2 doesn't know it is within an
fx
block.
I'm concerned it's easy to forget to
!
or
.bind()
.
b
.. darn, there used to be a really good example on arrow-kt.io of functions as values vs. code that is just executed
j
It would be an antipattern to explicitly add that autobinding manually. With arrow-meta I proposed that:
Copy code
fx {
  val fa: IO<A> = ...
  val x by fa // binds
  fa // also binds
}
Is valid syntax. This means forgetting
!
bind
is not a problem. Right now you are correct, it is easy to forget
Haskell does that as well. In do blocks it binds automatically
a
well, you need to use
<-
to do it.
j
Copy code
do
  let fa :: IO a = ...
  x <- fa -- binds
  fa -- binds
b
Ok, regarding "this is all sync code, it runs immediately anyway": there is a distinction that needs to be made
and that distinction is between composition of programs and effects. vs actual execution of everything
Using
fx
and lazy evaluation results in "programs" (and by programs, I mean small sets of function call chains) that can be chained/attached together easily
Taking a synchronous (runs immediately) application and translating it to use
IO
will still result in the application running immediately and synchronously, but the composition of functions inside the application and their evaluation is lazy instead of eager
e.g. the
IO.fx {}
example ^^ up there composes all of the functions together, and that whole block would be run synchronously and immediately upon calling
.unsafeRunSync
on the whole thing
but none of the functions are evaluated (lazy) until
.unsafeRunSync
n
Thanks. I think that makes sense, similar to Rx's Observable chain. But all I'm after at the moment is a clean replacement for Exceptions šŸ™‚
I think that's a common thing, given all the blog posts that direct us checked-Exception-refugees to Arrow.
j
For just exceptions and nothing else
Either
will work just fine. Just remember how to compose them
map
,
flatMap
,
traverse
,
mapN
,
fx
will be your most useful methods. Also use domain errors and stay away from wrapping everything regardless of need in
Either
. Then it will be a good replacement. If you want to use
IO
later on you will get a nice 1-1 mapping with
IO<E, A>
as soon as we start releasing 0.11 snapshots, which is planned right after the next release
n
Yes I read the blog post that mentioned BIO, sounds like it will be useful to us.
I think wrapping each function call in map/flatmap/traverse etc operators is quite a lot of boilerplate compared to one catch block at the bottom.
I like the sound of
IO
helping with exception handling but I don't want to have to convert all code to .map/.flatMap style.
b
My strategies have been to convert incrementally
a) provide functionality as a library that can be called from existing non-Arrow code, or
b) provide "programs in Either" or "programs in IO" that can be called with
.fold
or
.unsafeRunSync
that can be called at well-defined boundaries within try/catch
n
ah, does
.fold
run on
IO
and cause execution like
.unsafeRunSync
does?
b
.fold
runs on
Either
.unsafeRunSync
runs on
IO
but yes, they both force evaluation
Either
also has methods like
getOrElse
and
getOrHandle
that also force evaluation
n
Thanks. I like
fold{}
as it gives a clear place to handle error vs success, and
fx{}
as it provides a way to abort the block when anything returns a
Left
.
b
In general the data types / typeclasses provided by Arrow all include clear places to handle errors vs. success and ways of short-circuiting computation in the face of errors
it's one of the big benefits IME
n
...if you're using it in the way it's intended - functionally - yes. Trying to squeeze it into imperative code whilst keeping that code imperative breaks the neatness somewhat.
The risk of forgetting to
.bind()
or
!
or is concerning me :-(
j
it's bad, only thing that can stop that atm is code-review. It is a solved problem with arrow-meta comprehensions which will be in arrow very soon (afaik the plan is late this month or next month, but no promises^^)
n
cool, I'll keep a look out šŸ™‚ Thanks very much everyone for your help.
m
It's not often you forget the
!
or
bind
as the returned type will be an
Either
if you forget it, and therefore not directly usable by the next statement. Although it's more code, I prefer the explicitness of Either and custom error codes. The caller knows something can go wrong, and can decide how to handle it, or let it 'bubble' up. Exceptions still exist for truly exceptional, nothing the caller can do about it, situations. I like to think of Either as 'checked exceptions implemented in a usable, harder to abuse manner'
164 Views