<@U2E974ELT> Can sealed classes play well with flo...
# coroutines
p
@elizarov Can sealed classes play well with flows? Let me explain: Imagine we want to create a flow which is transparent regarding exceptions. We have this sealed class to represent either a success of failure:
Copy code
sealed class Result
data class Success(val image: Image) : Result()
object Error : Result()

class Image
And we have a suspending function to fetch an Image, which can throw an exception:
Copy code
suspend fun fetchImage(url: String): Image {
    // Returns either an Image instance or throws an exception
}
So we wrap this nasty function in another function which we'll use in our flows:
Copy code
suspend fun fetchResult(url: String): Result {
    return try {
        val image = fetchImage(url)
        Success(image)
    } catch (e: IOException) {
        Error
    }
}
Finally, we can use it:
Copy code
val urlFlow = flowOf("url1, url2", "retry")
val resultFlow = urlFlow.map { url -> fetchResult(url) }
So far, so good. However, out of curiosity, I tried to re-write the same flow above using the catch operator:
Copy code
val resultFlow: Flow<Result> = urlFlow.map { url ->
    val image = fetchImage(url)
    Success(image)
}.catch { e ->
    emit(Error)
}
This code does not compile. I try to emit an Error from catch (even if it's a valid Result instance). The compiler says: "Type mismatch, required Success". I know that it's because the inferred type of the upstream flow before catch is Flow<Success>. But in my humble opinion, we should be able to write some similar logic with a flow of some super type, and FlowCollectors being able to emit subclasses of the sealed class. Am I missing something?
a
I think you have to simply force the
map
operator to produce a
Flow<Result>
rather than a
Flow<Success>
, e.g.
urlFlow.map<Result> { … }
😀 1
☝️ 1
z
This isn't a flow issue. Any time a type is inferred from a value, the most specific type will be inferred.
a
sorry,
map
takes two type parameters, it would have to be
map<String,Result> { … }
or you can write
Success(image) as Result
which I admit looks a bit weird
k
Huh, are flows not covariant?
p
Thanks @araqnid, that's what I was missing. I can't believe I didn't know I could do that with map.
a
@Kroppeb they are:
Flow<out T>
I was wondering about that too. I thought that it should be possible to infer
Result
for that
catch
extension call, but apparently not.
k
I don't understand why it doesn't
Can you give
Result
as a type Param for
catch
?
a
Ah yes, you can do that. That’s nice, it’s simpler that giving two parameters to
map
You can get it to infer automatically by defining a catch operator that’s a bit more restrictive:
Copy code
fun <T> Flow<T>.catchAs(mapper: (Throwable) -> T): Flow<T> = catch { emit(mapper(it)) }
and then:
Copy code
val flow = flowOf("x")
            .map { value -> Outcome.Success("It was $value") }
            .catchAs { Outcome.Failure(it) }
z
catch
could handle this case if it had another type parameter. That might be worth filing/submitting a PR for (not just for catch, but in general). The current signature is:
Copy code
fun <T> Flow<T>.catch(
    action: suspend FlowCollector<T>.(cause: Throwable) -> Unit
): Flow<T>
But if it were this, then the compiler would be able to infer that
T
is
Success
but
R
must be the lowest-common-supertype of
Success
and
Error
, and your code would work as expected:
Copy code
fun <T : R, R> Flow<T>.catch(
    action: suspend FlowCollector<R>.(cause: Throwable) -> Unit
): Flow<R>
There are probably a lot of operators that could use that pattern.
k
The issue is that those are equivalent
Cause it's
flow<out T>
So each
flow<T>
is also
flow<R>
a
I think the problem is because it’s trying to solve for T which is used both in Flow<T> and FlowCollector<T> in that signature, and of course it’s contravariant for FlowCollector, so they only allow one solution for T. As @Zach Klippenstein (he/him) [MOD] points out, if you expand it so they have different (but related) parameters, then it can solve it by varying T to a superclass.
@Zach Klippenstein (he/him) [MOD] I’ve just tried it, but it doesn’t seem able to infer T and R if I define a catch extension like that. Not even if I try to give it a hint by assigning the result to a typed val of
Flow<Outcome>
(tried both with and without new inference)
z
Hm, maybe i messed up the bounds – i’m pretty sure I’ve used that trick in similar cases to solve this problem. Oh, i think the lambda needs the
@BuilderInference
annotation now, since it’s actually doing active inference.
a
hmm, still no joy; tried annotating it on the extension function and the collector parameter.