Compile Error using Elvis Operator with When based on Reified Type Parameter


#1

I’m not sure if this is a bug in the compiler or if I’m missing something obvious. This function compiles just fine:

inline fun <reified S> getParserForType() : (String)->S? {
    val valueParser : ((String) -> S?)? = when (S::class) {
        Float::class -> {s:String->s.toFloatOrNull() as S?}
        Double::class -> {s:String->s.toDoubleOrNull() as S?}
        else -> null
    }
    if( valueParser==null ) throw IllegalArgumentException()
    return valueParser
}

However, if I use the IDE suggestion to refactor to use the Elvis operator, as follows…

inline fun <reified S> getParserForType() : (String)->S? {
    val valueParser : (String) -> S? = when (S::class) {
        Float::class -> {s:String->s.toFloatOrNull() as S?}
        Double::class -> {s:String->s.toDoubleOrNull() as S?}
        else -> null
    } ?: throw IllegalArgumentException()
    return valueParser
} 

…I get two compile errors:

Error:(47, 25) Kotlin: Type mismatch: inferred type is (String) -> S? but Nothing? was expected
Error:(48, 26) Kotlin: Type mismatch: inferred type is (String) -> S? but Nothing? was expected

Am I missing something or is this a compiler bug?

On a related note… is there any more idiomatic way to check the type of a reified type parameter? Comparing it to the appropriate ::class seems to be the only way. Am I missing something?


#2

Hi,
why not getParserForFloat and gerParserForDouble?

Where is the advantage of using a type parameter if this parameter is reified?


#3

The actual use-case is much more complex; I simplified it here because I’m not free to share the original code (also, it’s quite large). Basically, this is dealing with a scene graph where objects in the graph can be of a large variety of different types, and each type of object can have a large variety of properties (also of various types). There are a lot of common operations that are performed on the properties (such as interpolation between different values over time) where the bulk of the code (90% or more) is common but certain things are different (for example, color properties are interpolated differently from angle properties). The idea is to have functions like:

fun <T:Property> someOperation( parser: T ) { /* ... */ }
inline fun <reified T:Property> someOperation() = someOperation(getParserForType(T))

There are various operations such as serialization, deserialization, keyframing, and various UX-related operations (like getting localized UI labels for properties, etc.). Some of these require a parser (to turn a string into an instance of the type) some require an interpolator (given two instances of the type and a float from 0…1 return a new interpolated instance of that type) etc. A parser or interpolator would be written for each type, and the remaining code (serialization, keyframing, looking up UI strings in the string table, etc.) would be common code not written for each type.

The idea of using reified type parameters and generics is twofold: (1) to keep the calling code consistent, and so that changes in property type can be made in a single place without having to fix code throughout the app, and (2) to keep most of the code common and avoid repetition (basically, DRY).

The use case might be, for example, reading attributes from an XML parser:

propertyFoo = xmlParser.readAttr("foo")
propertyBar = xmlParser.readAttr("bar")

In this case, propertyFoo and propertyBar could be different types, and type inference will make sure the right thing gets done.

It works great, and refactoring this way has made the codebase much simpler and more maintainable. I just ran into this odd glitch with the Elvis operator while working on it: As far as I can tell, the two versions of getParserForType that I posted should be functionally equivalent (and I think they should even generate the same bytecode) byte for some reason one compiles and the other doesn’t. Puzzling.


#4

Hi,
thank you for response.

Keep attention: propertyFoo and propertyBar variables must be the same type (effective instance type can be a subclass of it).


#5

Thanks for your reply too :slight_smile:

Why do they have to be the same type?

data class ColorProperty( val r:Float, val g:Float, val b:Float ) : Property
data class LocationProperty( val x:Float, val y:Float ) : Property

...

val propertyFoo : ColorProperty? = null
val propertyBar : LocationProperty? = null

...

propertyFoo = xmlParser.readAttr("foo")
propertyBar = xmlParser.readAttr("bar")

I have something similar to this in my current code base and it works fine :slight_smile:

(Edit: The advantage is that the property type is defined in just one place, so it can be changed without touching a lot of code)


#6

Sorry for the off topic,
this code compiles only if readAttr returns a subtybe of both ColorProperty and LocationProperty, generally this code doesn’t work.

I suspect that you don’t require a reified type - but probabily I wrong.


#7

No problem about going off topic. I’m happy to explain the use case (it seems you’re interested).

If readattr is generic, it indeed works just fine. You don’t need readAttr to return a common type. Tthis is in use in production code and compiles with no trouble. Consider if readAttr is defined like this:

fun <T> xmlParser.readAttr(key:String) : T {/* ... */ }

In this case you can call it for any type. Since the compiler already knows the type of the field that the result is being assigned to, you don’t need to explicitly give the type.

This is were the reified type parameter comes into play. If readAttr needs to know how to parse that parameter type, you can the above getParserForType() function. For example:

fun <T:Property> xmlParser.readAttr(key:String, parser: (String)->T ) : T {
   /* ... */ 
}

inline fun <reified T:Property> xmlParser.readAttr(key:String, parser: (String)->T ) : T =
    xmlParser.readAttr(key,getParserForType())

You are right that it doesn’t have to be a reified type. But then, you don’t ever need to use reified types in Kotlin. But there are times when you want to because it makes the code simpler. Keep in mind that as I said, there are a large number of different property types and there are a lot of properties on each node in the scene graph. Using this approach, you only have to worry about the property type where the field for that property is defined, and every place else automatically uses the correct type—it reduces the chance of bugs from typos, and makes refactoring easier if you need to change the type of a field (for example, from Double to Float). This is one of the strengths of Kotlin, in that in most cases you can leave out the type and the compiler can infer it.

Hope that explains the use case better :slight_smile:


#8

Thanks for your patience.

The type is T and is the same for both propertyFoo and propertyBar, right?

I cannot write

val xmlParser = ...
with(xmlParser.readAttr("foo")) {
    println("$r $g $b")
}

with(xmlParser.readAttr("bar")) {
    println("$x $y")
}

So any invocation of reified function getParserForType has the same T parameter.


#9

T is a placeholder for the actual type. In the case of propertyFoo the T would be ColorProperty and in the case of propertyBar the T would be LocationProperty.

In your example, propertyFoo and propertyBar aren’t used. This kind of situation never happens in the codebase that I’m working on, but if you wanted to use them in that way then you would need to give the type parameter explicitly:

val xmlParser = ...
with(xmlParser.readAttr<ColorProperty>("foo")) {
    println("$r $g $b")
}

with(xmlParser.readAttr<LocationProperty>("bar")) {
    println("$x $y")
}

However, I just searched the entire codebase and in practice there was never any case where the type had to be given explicitly like that: For debugging, we just use .toString() since they are data classes, and in the rest of the cases, there’s always an instance of the actual object there and the compiler can infer the type.

So you are right in theory, but in practice that doesn’t actually come up :slight_smile: (at least not for this particular application)