Mendess
11/18/2021, 12:07 PMsealed 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?Joffrey
11/18/2021, 12:28 PMfun <T> Result<T>.orDefault(d: T): T = when (this) {
is Result.Success<T> -> this.value
is Result.Failure<T> -> d
}
T
as a common ancestor of the result's actual T and the provided default value's type:
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.Mendess
11/18/2021, 12:43 PMJoffrey
11/18/2021, 12:53 PMResult<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:
val anyResult: Result<Any> = Result.Success<String>("str")
anyResult.orDefault( ... ) // will accept Any here, so Result<String> must accept Any
Mendess
11/18/2021, 12:56 PMJoffrey
11/18/2021, 1:00 PMval 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)Mendess
11/18/2021, 1:04 PMResult<String>
and I orDefault
with a String
, I get back a String
and not Any
right?Any
you'll just probably get a type error somewhere else, you still can't get past the compilerJoffrey
11/18/2021, 1:05 PMTobias Suchalla
11/18/2021, 1:15 PMsealed 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.Joffrey
11/18/2021, 1:25 PMT
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:
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):
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.