Custom ranged Int

I have a lot of small calculations emulating city life.

This isn’t helping or much meaningful.

Just go there, try both, profile, find the answer.

Not sure how I can make an extension here:
fun <T> MutableMap<T, RangedInt<*>>.inc(key: T, value: Int = 1) = merge(key, value, Int::plus)

Actually, I can’t even plus it:

fun main(){
    var myClass = RangedInt<Positive>(2)
    myClass ++
}

You need a separate extension per each range type and you need to use with(). Also, you need to pass RangedInt to merge(), not Int:

fun <T> MutableMap<T, RangedInt<Positive>>.inc(key: T, value: Int = 1) = with (Positive) {
    merge(key, rangedInt(value)) { v1, v2 -> v1 + v2 }
}
1 Like

Oh, it’s really verbose.

Sounds like your use-case is for RangedInt as a type :+1:


For others finding this content in the future, I wanted to put this here:

If your use-case is just to have an int property restricted to a range (which is not the same as creating a type with those restrictions), you can avoid creating a type entirely. The main question is if you want callers to understand the range limit via the Type or if it’s okay to just silently set it to the max/min of the range.

If your use-case is to convey the limited range in type, or maybe throw an exception for an attempt to set the value out-of-range, than you may prefer the RangedInt solution.

For limiting properties to specific values, the classic method is via getters/setters. This is the first correct answer and keeps your properties the same type.

//sampleStart
var myNumber: Int = 10
    set(value) {
        val max = 15
        val min = 10
        field = when {
            value > max -> max
            value < min -> min
            else -> value
        }
    }
//sampleEnd
fun main() {
    println(myNumber)
    myNumber = 12
    println(myNumber)
    myNumber = 8
    println(myNumber)
    myNumber = 22
    println(myNumber)
}

Next, you might find that you want to reuse some limiting logic. Pretty easy to call another function to do the limiting (ex. the stdlin coerceIn). That’s the second step while still keeping our API the same and allowing good reuse.

//sampleStart
var myNumber: Int = 10
    set(value) { field = value.coerceIn(10, 15) }
//sampleEnd
fun main() {
    println(myNumber)
    myNumber = 12
    println(myNumber)
    myNumber = 8
    println(myNumber)
    myNumber = 22
    println(myNumber)
}

Lastly, maybe you want to do a more complex observation of the property? Use property delegation and make use of Kotlin’s delegation syntax sugar. Possibly use one of the built-in delegates instead of creating a new one.

// Some existing delegates
var myNumber: Int by vetoable(/*...*/)
var myNumber: Int by observable(/*...*/)

// Maybe make a custom delegate
var myNumber: Int by coercedValue(/*...*/)
var myNumber: Int by GreaterThanValue<Int>(0)

The key here is that we do not change the API for theoretical performance or implementation details. The details that this Int is limited shouldn’t leak out like they do if we type values as a kind of 'RangedInt`. We don’t have to implement any operators or make our callers extract the backing value.


As others have mentioned, don’t preoptimize. It’s possible you’ve already spent more developer time trying to save less than a millisecond when your final app while another area could save you thousand times more.

You’ll see it a lot with new coders. They often fall into a trap of thinking they should use the optimal algorithms instead of focusing on components and interfaces. Then they fall into a deeper trap of letting performance implementation leak out into their interfaces :grimacing:

Choose the best interface first → implement a clean & quick solution → see what’s worth optimizing → then go back for the optimal solution if it’s worth it.

2 Likes

Yeah, I’m doing something like this now.

var myNumber: Int = 10
    set(value) { field = value.coerceIn(10, 15) }

But I don’t familiar much with your delegates proposal. Maybe some samples needed. Anyway, how your solution can be used in maps?

For maps, I’d first need to know what I’m trying to do.

Let’s say the answer is that I want “a map of T keys to a limited range of Int values. No error on out-of-bounds sets from callers, silently coerced into the range”. Well that sure looks like its own type.

val charToDigit = RangeLimitedMap<Char>(0..9) // or (min = 0, max = 9) or anything else.

Then the implementation of the put would just limit key values to the range.

We can still specify the type of key… but why do we say all values are Int? Isn’t it any comparable value? So next we might do:

val playingCardsToInt = RangeLimitedMap<Char, Int>(1, 52)
val gradesToPoints = RangeLimitedMap<String, Double>(0.0, 4.0) // We can limit to Double range or Int range

Next we might realize we want to customize the values more dynamically than just limiting them to a comparable range:

val playingCardsToInt = ValueLimitedMap<Char, Int> { value -> value.coerceIn(1, 52) }
val alphabetToInt = KeyLimitedMap<Char, Int> { key -> if (key.isDigit()) null else key }

And finally, we might realize that our implementation is just going to delegate to a map and it’s pretty easy to confine both the keys and map in the same class:

class ConfinedMutableMap<K, V>(
    private val backingMap: MutableMap<K, V> = mutableMapOf(), 
    val confineKey: (K) -> K, 
    val confineValue: (V) -> V
) : MutableMap<K, V> by backingMap {
    override fun put(key: K, value: V): V? {
        return backingMap.put(confineKey(key), confineValue(value))
    }
}

fun main() {
    val myData = ConfinedMutableMap<String, Int>(
    	confineKey = { key: String -> key.toLowerCase() }, // Only lowercase keys
        confineValue = { value: Int -> value.coerceIn(10..15) } // Only values between 10 and 15
    )
    println(myData.entries.joinToString())
    myData["Hello World"] = 5
    println(myData.entries.joinToString())
    myData["WAS UPPERCASE"] = 20
    println(myData.entries.joinToString())
    myData["this_was_lower.and.12"] = 12
    println(myData.entries.joinToString())
}

EDIT: You could also leave the data as-is and confine the value when when pop’ing. Which would prompt me to make extension functions like map.asConfined({/*...*/}, {/*...*/})

2 Likes

Generally, I think it all depends on what do you need to optimize. CPU usage? Memory consumption? Heap allocations (GC)? And how much are you read to pay for it by degrading the code readability, maintenance and increasing the development time.

I think you can’t really optimize CPU in this case. I doubt it makes any difference if you use min() and max(), coerceIn() or if-elses. Manual if-elses could be potentially faster as we don’t need a function call, but again, I doubt we could observe any real difference.

Both memory consumption and allocations could be improved by using a primitive instead of an object. But then we have to somewhere store min and max. One way is to store it in the type itself. This is the trick made by @kyay10 . Their solution is optimized for easy creating new ranges, but then we have to use with() or otherwise reference the object to access operators. Alternatively, we could create entirely separate types - this way we need more work to create new ranges, but then we could use operators directly, without with().

Of course, using static types to store min and max has its drawbacks:

  • We have to know ranges upfront, we can’t create them dynamically.
  • Code has to know the type it is working with, it is tightly coupled to the range type.
  • We can’t write generic code, we need to duplicate the same code for each separate range.

Alternatively, we could use a primitive long and encode all 3 values (value, min, max) in it, for example by using 21 bits for each of them. This is still a fully dynamic solution, we don’t have to know ranges upfront, we can write generic code that works with any range, but of course, we decrease int sizes and we consume additional CPU to decode values (but bit shifting is very fast).

For all above solutions where we use primitives instead of objects, benefits are almost entirely lost when we have to box values. That means e.g. using them in lists, maps or other collections. In order to benefit fully from them, you would have to create your own implementation of a map or search for existing implementations based on primitives.

If we need to store the data in a collection, it doesn’t matter that much if we use integer, class inlined to integer, class with one integer or class with 3 integers. in all cases we have the same number of allocations. In the last case we theoretically consume more memory by additional 2 integers, but in Java this is really nothing. I believe there is ~16 bytes of overhead per object allocation. That means a boxed primitive consumes ~20 bytes and an object with 3 ints uses ~28 bytes. But of course this is only the value itself. If using a map, for each item we need to store the value (20/28B), key (String in your case, so 40B (?) + chars), map node (32B in HashMap, 40B in LinkedHashMap which is default in Kotlin). My calculations may be entirely wrong, maybe in 64bit we actually use 8 bytes for references (I don’t know, I assumed 4 bytes), but the point is: we need more than 100 bytes per each item in your map, so saving 8 bytes doesn’t really change too much.

But frankly speaking, all I said above is pure speculation and is probably more or less untrue. JVM is too complicated to make such theoretical analysis. With all these JITs and GC the only way to be sure is by running benchmarks and even this is not that easy to do properly. In most cases we should not really do such microoptimizations and instead focus on the code quality, readability and maintainability. Even when optimizing, that usually makes more sense by choosing right data structures and algorithms and not by looking for the best way to perform a min operation.

1 Like

This looks good. Previously I asked about maps consistency, this solution could solve it.

Other way is to use a structure with many primitives and limit them there. This class combines ranged Ints and parameters reflection:

open class Custom {
    val min = 0
    open val max = 10 // can be static const

    val _values = MutableList(2) { Random.nextInt(min, max) }
    val values: List<Int> = _values

    var foo: Int
        get() = values[0]
        set(value) {
            setInRange(0, value)
        }
    var bar: Int
        get() = values[1]
        set(value) {
            setInRange(1, value)
        }

    private fun setInRange(index: Int, value: Int) {
        _values[index] = ranged(value)
    }

    private fun ranged(value: Int) = value.coerceIn(min, max)
}

Is it possible to override Number, Comparable same way with native annotations?