I'm having some problems with variance. I'm making...
# getting-started
m
I'm having some problems with variance. I'm making my own result type:
Copy code
sealed class Result<out T> {
  class Success<out T>(val value: T): Result<T>()
  class Failure<out T>(val error: MyError): Result<T>()

  fun orDefault(d: T): T = when (this) {
    is Success<T> -> this.value
    is Failure<T> -> d
  }
}
I understand why it doesn't work, T is in
in
position when I marked it as
out
, what I want to know is: Is there some syntax to make
T
invariant or contravariant just for that method?
j
What if you make it an extension function?
Copy code
fun <T> Result<T>.orDefault(d: T): T = when (this) {
    is Result.Success<T> -> this.value
    is Result.Failure<T> -> d
}
The main problem with the extension approach is that it allows to provide default values of different types by inferring
T
as a common ancestor of the result's actual T and the provided default value's type:
Copy code
val successInt: Result<Int> = Result.Success(42)
val errorStr: Result<String> = Result.Failure<String>(MyError())

println(successInt.orDefault("a string!"))
println(errorStr.orDefault(42))
This is because
T
is
out T
in
Result
, so a
Result<Int>
IS-A
Result<Any>
. I don't know if this consequence would be OK for you.
m
If there is no other way, I'll give it a go and see how it feels, thanks!
j
Actually this makes sense. Let's assume you could define that function like you wanted to. Now take a
Result<String>
. Because
Result
is
out T
, you can assign this object to a variable of type
Result<Any>
. Now you can call
orDefault
on that variable, and it has to accept
Any
as input, so this means in
Result<String>.orDefault()
you can get a value of type
Any
- you can't restrict it to T:
Copy code
val anyResult: Result<Any> = Result.Success<String>("str")

anyResult.orDefault( ... ) // will accept Any here, so Result<String> must accept Any
m
yeah, that makes sense
tho it's a bit annoying 😅
but such is life
j
You can't have your cake and eat it too 😄 so you have to either forbid
val anyResult: Result<Any> = Result.Success<String>("str")
by removing the
out
, or accept any type in
orDefault()
(which is totally OK IMO, because the return type will still be very useful)
Yeah the more I think of it, the more I believe it's actually useful to allow defaulting to anything. There is no real reason to limit the result to some particular type, as long as the return type is consistent with the type of the default value chosen by the caller. With union types it would be even more useful, but that's not there yet 😄
m
if I have
Result<String>
and I
orDefault
with a
String
, I get back a
String
and not
Any
right?
👌 1
alright, than it's fine yeah, it's not like it's error prone, if you make a mistake and get
Any
you'll just probably get a type error somewhere else, you still can't get past the compiler
j
Yes exactly, that's the beauty of it. That's why I think it's fine. It's still strict on the return type if you're using the default value type matching the result's type, but it also allows you to get a wider return type if that's what you want by providing any default value you want
t
As to the original question, Intellij Idea suggests `@UnsafeVariance`:
Copy code
sealed class Result<out T> {
    class Success<out T>(val value: T): Result<T>()
    class Failure<out T>(val error: Exception): Result<T>()

    fun orDefault(d: @UnsafeVariance T): T = when (this) {
        is Success<T> -> this.value
        is Failure<T> -> d
    }
}
Then
orDefault
is limited to be of the original
T
. But to be honest, I'm not up to speed with variance and can't comment on potential drawbacks.
👍 1
j
Yeah that's actually an option too. It doesn't guarantee that
T
is the original
T
- because your instance could be assigned to a variable with wider
T
type. But it still works because
out
guarantees that
T
in this method will at least be a supertype of the actual
T
. And given what is done inside the method it's ok to return `this.value`if
T
is a supertype of the actual value's type. So in essence it prevents direct
orDefault()
call with a different type, but still doesn't protect against:
Copy code
val anyResult: Result<Any> = Result.Success<String>("str")

anyResult.orDefault(42) // returns Any, or maybe Comparable
So yeah it can be useful if you want to make it harder to use default values of different types. But I'm not sure this is really necessary. It's like using
when
expressions with different types in different branches (it's actually literally this):
Copy code
val value = when(...) {
   ... -> 42
   ... -> "str"
}
To me this is OK, since anyway the type of
value
will be as specific as it can be. So the same goes for
orDefault()
IMO.
👍 1