I am playing around with different ways of definin...
# codereview
j
I am playing around with different ways of defining a factory function to map a scalar to an enum. For instance:
Copy code
enum class OpCode(val code: Int) {
    QUERY(0), IQUERY(1), STATUS(2), NOTIFY(4), UPDATE(5), UNSUPPORTED(-1);
}

fun Int.toOpCode(): OpCode {
    return OpCode.values().find { it.code == this } ?: OpCode.UNSUPPORTED.also {
        log.error { "Unsupported DNS OpCode $this" }
    }
}
This uses a receiver function, which I like in general, and obviously there are many different ways of writing this function (such as
fun toOpcode(code:Int): Opcode
) etc. Now, it occurred to me that this is also possible:
Copy code
enum class OpCode(val code: Int) {
    QUERY(0), IQUERY(1), STATUS(2), NOTIFY(4), UPDATE(5), UNSUPPORTED(-1);

    companion object {
        operator fun invoke(code: Int): OpCode {
            return values().find { it.code == code } ?: UNSUPPORTED.also {
                log.error { "Unsupported DNS OpCode $this" }
            }
        }
    }
}
This allows the caller to map an
Int
to an
OpCode
using what looks like a constructor:
val code = OpCode(0)
What would you say if you saw this in a pull request? Is this clever and nice, or horrible and confusing and just showing off? I keep switching sides myself... 🙂
m
I don't really like either. Having it as extension on
Int
means it's available on every integer, but probably doesn't make sense most of the time. The other approach would just confuse me, because I would think I am instantiating something. I would go with
fun of(code: Int): OpCode
in the companion object, resulting in call like this:
val code = OpCode.of(0)
đź‘Ť 4
j
Yeah, those are valid points, and your suggestion is definitely the most straightforward. Thanks for the feedback!
đź‘‹ 1
m
@Milan Hruban an extension function on Int can be restricted to just the file where the conversion code needs it, otherwise it needs to be explicitly imported everywhere else, so I don’t think it will pollute integers API globally. The solution with a companion function feels a lot like Java but it’s good anyway.
@Joris PZ the “constructor lookalike” solution is used in the stdlib for example:
Copy code
inline fun <T> List(
    size: Int, 
    init: (index: Int) -> T
): List<T>
not a companion but a top-level function. Gets the job done while simulating a ctor, I think it’s a nice trick and perfectly reasonable if you didn’t know how it’s implemented. The same could apply to your enum: who cares if it’s emulating a ctor, as long as it works and reads clearly? In Java we were forced to use static factory methods, but in Kotlin we have more choices.
m
@Matteo Mirk well if it's a
private
function, then I would also probably choose extension, but I doubt that. What's the difference in having to be explicitly imported? It still pops up in autocomplete (and automatically adds the import, if you select it, likely without you noticing).
m
If you have it imported by the IDE, it’s explicitly visible in the code, but it doesn’t mean it’s available globally. I would take care here distinguishing IDE facilities from the actual language grammar.
👆 1
m
@Matteo Mirk I am not following - what do you mean it's not available globally? I can see it anywhere in the project, right? Or if it's imported, it's not considered available globally?
j
You have to explicitly import
toOpcode
but IntelliJ will let you know every potential extension function on an
Int
, making it seem like it's available everywhere, when technically it isn't.
âž• 1
I dislike overloading
invoke
because it results in behavior that is different from what is expected. I'd use the
of
pattern and the extension function for ease-of-use i.e.
Copy code
fun Int.toOpCode() = OpCode.of(this)
m
yes I understand I have to add an import, but when is this relevant?