https://kotlinlang.org logo
m

Mark Fisher

12/29/2020, 10:21 AM
Hi Kotliners, A post xmas question for anyone alive in this time of limbo before the new year! I've got a generics question. I can't understand why there's an error in the following:
Copy code
interface HexData
object EmptyHexData : HexData
data class Hex(val q: Int, val r: Int, val s: Int)

interface HexDataStorage<T: HexData> {
    fun addHex(hex: Hex)
    fun addHex(hex: Hex, data: T): Boolean
}

class DefaultHexDataStorage<T: HexData> : HexDataStorage<T> {
    private val storage = LinkedHashMap<Hex, T>()

    override fun addHex(hex: Hex) {
        storage[hex] = EmptyHexData // <-- ERROR
    }

    override fun addHex(hex: Hex, data: T): Boolean {
        val previous = storage.put(hex, data)
        return previous != null
    }
}
In the function addHex(hex: Hex), I cannot use "EmptyHexData", and I don't understand why. AFAIU It's a sub-type of HexData, so I thought it would comply with the requirement of being a
T
in this case. What's wrong, and how do I fix it? I tried using a class too, e.g.
class EmptyHexDataClass: HexData
but that didn't work either.
g

Giorgos Neokleous

12/29/2020, 10:32 AM
Do you need generics at all in your example? Seems like you can just get by without them
Copy code
interface HexDataStorage {
    fun addHex(hex: Hex)
    fun addHex(hex: Hex, data: HexData): Boolean
}
class DefaultHexDataStorage : HexDataStorage {
    private val storage = LinkedHashMap<Hex, HexData>()
    override fun addHex(hex: Hex) {
        storage[hex] = EmptyHexData
    }
    override fun addHex(hex: Hex, data: HexData): Boolean {
        val previous = storage.put(hex, data)
        return previous != null
    }
}
y

Yev Kanivets

12/29/2020, 10:33 AM
addHex(hex: Hex)
takes
Hex
, not
T
, and
EmptyHexData
doesn’t inherit from
Hex
, but from
HexData
.
😅 1
👎 2
v

Vampire

12/29/2020, 10:34 AM
@Yev Kanivets I think you misread the example
m

Mark Fisher

12/29/2020, 10:34 AM
@Yev Kanivets that's not right here, the type is in the storage.
v

Vampire

12/29/2020, 10:35 AM
@Mark Fisher
T
is
HexData
or a subtype of it
So if you have
interface MyHexData : HexData
and then have a
DefaultHexDataStorage<MyHexData>
that would be legal
But then you couldn't assign
EmptyHexData
as it is a
HexData
, but not a
MyHexData
That's why it says "you have
EmptyHexData
but
T
was expected"
m

Mark Fisher

12/29/2020, 10:37 AM
@Giorgos Neokleous I did consider a non-generics version, and instead have a Maybe<T> there, but still would like to get this working without the Maybe class which I think adds noise. @Vampire isn't EmptyHexData a subtype of HexData?
v

Vampire

12/29/2020, 10:37 AM
So it's back to the question of Girogos, why generics at all
Of course it is, but not of
MyHexData
as I just explained
If you create a
DefaultHexDataStorage<MyHexData>
your map will have runtime type
LinkedHashMap<Hex, MyHexData>
and
EmptyHexData
is not a
MyHexData
but a sibling
m

Mark Fisher

12/29/2020, 10:39 AM
ah, that explains it better, when considering the actual implementation, i need a full type.
v

Vampire

12/29/2020, 10:40 AM
So again the question of Giorgos, why do you need generics at all? Or a
Maybe
whatever that should be.
Why wouldn't his suggestion work for you?
m

Mark Fisher

12/29/2020, 10:41 AM
It is good enough to not use Generics, but I couldn't understand why the posted version wasn't working. You've explained that well enough now for me to understand, so thanks! 🙂
👌 1
r

Rob Elliot

12/29/2020, 11:11 AM
In terms of what you’re trying to achieve with the generic type, you’re probably better off using
null
to represent `EmptyHexData`:
Copy code
private val storage = LinkedHashMap<Hex, T?>()
    override fun addHex(hex: Hex) {
        storage[hex] = null
    }
m

Mark Fisher

12/29/2020, 2:52 PM
@Rob Elliot That's why I used Maybe<T>, to avoid the use of nulls:
Copy code
private val storage = LinkedHashMap<Hex, Maybe<T>>()

    override fun addHex(hex: Hex) {
        storage[hex] = Maybe.empty()
    }

    override fun addHex(hex: Hex, data: T): Boolean {
        val previous = storage.put(hex, Maybe.of(data))
        return previous != null
    }
Although my current implementation doesn't need generics, so I've stripped it back to simple HexData types directly.
v

Vampire

12/29/2020, 2:56 PM
And where is the advantage to wrap the entries instead of using a nullable type?
m

Mark Fisher

12/29/2020, 2:58 PM
I'm averse to using nulls and nullable types.
y

Youssef Shoaib [MOD]

12/29/2020, 3:37 PM
Using null and nullable types is idiomatic in Kotlin instead of using a maybe since the language has complete support for it. This isn't only my opinion, but also the opinion of actual fp experts like the whole Arrow team who decided to deprecate Maybe in favour of the native Kotlin nullables since the compiler tracks the nullable types and checks any calls that you try to do on them automatically (similar to how they decided to go with suspend instead of an
IO
wrapper since the compiler automatically tracks effects with suspend)
n

Nir

12/29/2020, 3:40 PM
I'd agree that nullable types are idiomatic in Kotlin broadly speaking but nullables can be a bit of a pain in generic contexts
and in particular Map<Hex, HexData?> can be pretty annoying
a lot of the extension functions for Map (and MutableMap) don't do what you'd expect, and basically act as though an entry to null isn't actually in the map
I actually agree with the approach here, it leads to less confusing code