avb
avb

Reputation: 1751

Custom 'typesafe' Int Types

What I would like to have is two different integer types which are semantically distinguishable.

E.g. in this code a 'Meter' type and a 'Pixel' int type

typealias Meter = Int
typealias Pixel = Int

fun Meter.toPixel() = this * 100
fun Pixel.toMeter() = this / 100

fun calcSquareMeters(width: Meter, height: Meter) = width * height
fun calcSquarePixels(width: Pixel, height: Pixel) = width * height

fun main(args: Array<String>) {
    val pixelWidth: Pixel = 50
    val pixelHeight: Pixel = 50

    val meterWidth: Meter = 50
    val meterHeight: Meter = 50

    calcSquareMeters(pixelWidth, pixelHeight) // (a) this should not work

    pixelWidth.toPixel() // (b) this should not work
}

The problem with this solution is

(a) that I can call calcSquareMeters with my 'Pixel' type which I don't want to be possible and

(b) that I can call the toPixel() extension function which I only want to have for my 'Meter' type on my 'Pixel' type which I don't want to be possible.

I guess this is the intended behaviour of typealias, so I guess to achieve my goal I have to use something different than typealias...

So how can I achieve this?

Upvotes: 1

Views: 126

Answers (5)

anfark
anfark

Reputation: 116

I would also go with the solution from TheOperator. But I would like to add the inline keyword to the operator functions. By doing so you could avoid a virtual function call when ever you use this operators.

inline operator fun <T : MetricType<T>> T.plus(rhs: T) = new(this.value + rhs.value)
inline operator fun <T : MetricType<T>> T.minus(rhs: T) = new(this.value + rhs.value)
inline operator fun <T : MetricType<T>> T.times(rhs: Int) = new(this.value * rhs)

Upvotes: 0

TheOperator
TheOperator

Reputation: 6506

In addition to the existing answer: If you have a lot of common functionality between the two types and don't want to duplicate it, you can work with an interface:

interface MetricType<T> {
    val value: Int

    fun new(value: Int): T
}

data class Meter(override val value: Int) : MetricType<Meter> {
    override fun new(value: Int) = Meter(value)
}

data class Pixel(override val value: Int) : MetricType<Pixel> {
    override fun new(value: Int) = Pixel(value)
}

Like this, you can easily define operations on the base interface, such as addition, subtraction and scaling:

operator fun <T : MetricType<T>> T.plus(rhs: T) = new(this.value + rhs.value)
operator fun <T : MetricType<T>> T.minus(rhs: T) = new(this.value + rhs.value)
operator fun <T : MetricType<T>> T.times(rhs: Int) = new(this.value * rhs)

The combination of interface and generics ensures type safety, so you do not accidentally mix types:

fun test() {
    val m = Meter(3)
    val p = Pixel(7)

    val mm = m + m // OK
    val pp = p + p // OK
    val mp = m + p // does not compile
}

Keep in mind that this solution comes at a runtime cost due to the virtual functions (compared to rewriting the operators for each type separately). This in addition to the overhead of object creation.

Upvotes: 5

Alexey Romanov
Alexey Romanov

Reputation: 170839

There is a proposal (not yet guaranteed to be accepted) to add inline classes for this purpose. I.e.

@InlineOnly inline class Meter(val value: Int)

will really be an Int at runtime.

See https://github.com/zarechenskiy/KEEP/blob/28f7fdbe9ca22db5cfc0faeb8c2647949c9fd61b/proposals/inline-classes.md and https://github.com/Kotlin/KEEP/issues/104.

Upvotes: 1

zsmb13
zsmb13

Reputation: 89628

Indeed, typealiases don't guarantee this sort of type safety. You'll have to create wrapper classes around an Int value instead to achieve this - it's a good idea to make these data classes so that equality comparisons work on them:

data class Meter(val value: Int)
data class Pixel(val value: Int)

Creation of instances of these classes can be solved with extension properties:

val Int.px
    get() = Pixel(this)

val pixelWidth: Pixel = 50.px

The only problematic thing is that you can no longer directly perform arithmetic operations on Pixel and Meter instances, for example, the conversion functions would now look like this:

fun Meter.toPixel() = this.value * 100

Or the square calculations like this:

fun calcSquareMeters(width: Meter, height: Meter) = width.value * height.value

If you really need direct operator use, you can still define those, but it will be quite tedious:

class Meter(val value: Int) {
    operator fun times(that: Meter) = this.value * that.value
}

fun calcSquareMeters(width: Meter, height: Meter) = width * height

Upvotes: 3

crgarridos
crgarridos

Reputation: 9273

From kotlin doc:

Type aliases do not introduce new types. They are equivalent to the corresponding underlying types. When you add typealias Predicate and use Predicate in your code, the Kotlin compiler always expand it to (Int) -> Boolean. Thus you can pass a variable of your type whenever a general function type is required and vice versa

This means that there isn't possible check over your typealias, and you are rally declaring your extensions functions as:

fun Int.toPixel() = this * 100
fun Int.toMeter() = this / 100

fun calcSquareMeters(width: Int, height: Int) = width * height
fun calcSquarePixels(width: Int, height: Int) = width * height

I fear the only way to achieve that you want is implementing an extra class for each type.

Upvotes: 0

Related Questions