And there's no `Fx` instance for `EitherT`
# arrow
d
And there's no
Fx
instance for
EitherT
s
I think they are thinking about introducing an
IO<E, A>
in a future version of arrow
d
Do you know if
E
has to be
Throwable
?
s
Right now,
IO<E>
is like a asynchronous `Try<E>`…. only supports Throwables for the ‘error’ type, if i’m not mistaken. The
IO<E, A>
is not in arrow now, maybe in a future version… I remembe some discussions going on about this.
d
I started to implement my own version of
IO
that accepted an error type param, but when I realized that effectful comprehensions required the
MonadThrow
typeclass, and my
MonadError
instance didn't use throwables, I had to give up.
Then I decided to try using
suspend
, and save
IO
for the edge of my program, and just use
Either
as my error monad, but I soon found out that
Either
comprehensions cannot have side effects
Even when the comprehension is inside a
suspend
function
So to do side effects, I can create
IO
inside the
Either
, call
sequence
to convert
Either<IO>
into
IO<Either>
, use a mix of
IO
and
Either
comprehensions, then call
unsafeRunAsync
inside the suspend function to extract the value from the IO, but that's really messy
I could create my own
Fx
instance for
EitherT
but I'd run into the
MonadThrow
problem again.
p
you may find this PR interesting: https://github.com/arrow-kt/arrow/pull/1478
it introduces a new constructor to Either that supports suspend functions
d
Oh fantastic
r
IO will support user parametric errors that are not Throwables and also handle Throwables in 0.10.1 after the upcoming release
So we will have bifunctor IO and type classes for effects similar to ZIO and BIO
d
Argh! So
Either.catch
got me part of the way, but
EitherFx
still doesn't permit
!effect
calls
so I can't call
Either.catch
from inside an
Either
comprehension
Copy code
override suspend fun <T : Any> getFromCache(type: KClass<T>, key: String): Either<CacheGetError, T?> {
        val valueE = Either.catch(::CacheReadError andThen ::CacheGetError) {
            redis
                .get("$keyCache:$key")
                .await()
        }

        val serializerE = Json.context.getContextual(type).toOption()
            .toEither{ CacheGetError(NoSuchSerializer(type)) }

        return Either.fx<CacheGetError>().fx {
            val value = !valueE
            val serializer = !serializerE
            suspend { 
                Either.catch(::CacheDeserializationError andThen ::CacheGetError) {
                    (value?.let { withContext(<http://Dispatchers.IO|Dispatchers.IO>) { Json.nonstrict.parse(serializer, value) } })
                } 
            }
        }.fix().flatMap { it() }
    }
So, if I push all the suspending into the outer program, it compiles. But the comprehension won't let me use side effects directly.
Better:
Copy code
override suspend fun <T : Any> getFromCache(type: KClass<T>, key: String): Either<CacheGetError, T?> {
        val valueE = Either.catch(::CacheReadError andThen ::CacheGetError) {
            redis
                .get("$keyCache:$key")
                .await()
        }

        val serializerE = Json.context.getContextual(type).toOption()
            .toEither{ CacheGetError(NoSuchSerializer(type)) }

        return Either.applicativeError<CacheGetError>()
            .tupled(valueE, serializerE)
            .flatMap { (value, serializer) ->
                Either.catch(::CacheDeserializationError andThen ::CacheGetError) {
                    (value?.let { withContext(<http://Dispatchers.IO|Dispatchers.IO>) { Json.nonstrict.parse(serializer, value) } })
                }
            }
    }
r
Yeah all that is solved with bifunctor io
And effect takes suspend -> Either<E,A>
d
What's the expected timing on that feature?
I expect
fx
will need additional work since
!effect
currently requires
MonadThrow
r
Yes the type classes will change and you'll use the concurrent fx or IO / suspend
The release we are hoping is ready by next week and after that we will start with BIO which should not take more than a few days to implement and adapt the typeclasses and laws
We can do another release as soon as we have BIO
d
Since
flatMap
is inlined, I can work around it that way:
Copy code
override suspend fun <T : Any> putInCache(
        type: KClass<T>,
        key: String,
        value: T?,
        seconds: Long
    ): Either<CachePutError, Unit> {
        val serializerE =
            Json.context.getContextual(type).toOption()
                .toEither{ CachePutError(NoSuchSerializer(type)) }

        val jsonStringE = serializerE.flatMap { serializer ->
            Either.catch(::CacheSerializationError andThen ::CachePutError) {
                value?.let { Json.plain.stringify(serializer, value) }
            }
        }

        val resultE = jsonStringE.flatMap { jsonString ->
            Either.catch(::CacheWriteError andThen ::CachePutError) {
                redis.set(
                    "$keyCache:$key",
                    jsonString,
                    SetArgs().nx().ex(seconds)
                ).await()
            }
        }

        return resultE
            .ensure({CachePutError(KeyExistsInCache(key))}) { result ->
                result == "OK"
            }
            .map(constant(Unit))
    }
r
Yeah that works in the interim but it will be much nicer soon