How would you enable the `in` operator for membership tests against an interval of your own domain type, and what are the correctness pitfalls (Comparable consistency, half-open intervals, thread safety)?
answer
- `in` needs an `operator fun contains` receiver
- Comparable + ClosedRange -> default contains free
- custom interval class for half-open `[start, end)`
- compareTo must be total/transitive; use compareValuesBy (no subtraction overflow)
- prefer immutable value types; document inclusivity
basics
~10 sMake your type Comparable, then build a ClosedRange (or define your own operator fun contains). After that, value in interval just works. Ensure your ordering is consistent and total.
solid answer
~40 sTwo routes give `in` support. (1) Make your domain type implement `Comparable<T>` and create a `ClosedRange<T>` (e.g. via a `rangeTo` operator returning an anonymous `ClosedRange`); the interface's default `contains` uses your `compareTo`. (2) Define a custom interval class with your own `operator fun contains(value): Boolean` — useful for **half-open** intervals `[start, end)` that `ClosedRange` (inclusive) can't express. Pitfalls: `compareTo` must be a **total order**, consistent with `equals` ideally, and free of overflow (subtracting fields can overflow — compare instead); a non-transitive or partial order makes membership undefined. For floating-point fields, beware NaN. If the interval is mutable, `contains` is not thread-safe; prefer immutable value types. Document inclusivity explicitly. This is how stdlib supports `LocalDate`/`BigDecimal`/`String` ranges, and how you'd expose a `VersionRange`, `TimeWindow`, or `IpRange` membership API.
code
kotlin · 12 linesdata class SemVer(val major: Int, val minor: Int, val patch: Int) : Comparable<SemVer> {
override fun compareTo(other: SemVer): Int =
compareValuesBy(this, other, SemVer::major, SemVer::minor, SemVer::patch)
}
class VersionRange(val from: SemVer, val toInclusive: SemVer) {
operator fun contains(v: SemVer): Boolean = v >= from && v <= toInclusive
}
val supported = VersionRange(SemVer(1, 0, 0), SemVer(2, 9, 9))
println(SemVer(2, 0, 0) in supported) // true
println(SemVer(3, 0, 0) !in supported) // truego deeper
Can state that making a type Comparable and building a ClosedRange enables in.
Implements operator fun contains and chooses between ClosedRange and a custom interval for inclusivity.
Handles compareTo correctness (total order, overflow, NaN) and picks immutable value types.
Designs the membership API as a domain contract — inclusivity, ordering, immutability, thread safety — and weighs ClosedRange reuse vs. a bespoke type across the codebase.
## Goal: `value in myInterval` Because `in` desugars to `contains`, you just need a receiver that exposes `operator fun contains`. There are two idiomatic ways. ## Route 1 — Comparable + ClosedRange If your type is `Comparable`, you get the **default** `ClosedRange.contains` (`value >= start && value <= endInclusive`) for free. ```kotlin data class SemVer(val major: Int, val minor: Int, val patch: Int) : Comparable<SemVer> { override fun compareTo(other: SemVer): Int = compareValuesBy(this, other, SemVer::major, SemVer::minor, SemVer::patch) } // rangeTo via an anonymous ClosedRange operator fun SemVer.rangeTo(end: SemVer): ClosedRange<SemVer> = object : ClosedRange<SemVer> { override val start = this@rangeTo; override val endInclusive = end } val supported = SemVer(1,0,0)..SemVer(2,3,9) println(SemVer(1,5,0) in supported) // true, uses compareTo ``` `compareValuesBy` builds a correct lexicographic comparator without manual subtraction (which can overflow). ## Route 2 — your own interval type (half-open, custom rules) `ClosedRange` is always **inclusive** on both ends. For a **half-open** `[start, end)` window — common for time ranges — define your own `contains`: ```kotlin class TimeWindow(val start: Instant, val endExclusive: Instant) { operator fun contains(t: Instant): Boolean = t >= start && t < endExclusive } println(now in TimeWindow(t0, t1)) // [t0, t1) ``` This is where a custom type beats `ClosedRange`: you control inclusivity, wrap-around (IP/CIDR), or units. ## Correctness pitfalls - **Total, consistent order:** `compareTo` must be reflexive, antisymmetric, transitive, and total. A partial/non-transitive order makes `>= start && <= end` meaningless. Keep it **consistent with `equals`** to avoid surprises in sets/sorting. - **Overflow:** never `return this.value - other.value` for `Int` fields — use comparison or `compareValuesBy`. - **NaN / floating fields:** NaN breaks ordering; mirror `ClosedFloatingPointRange`'s explicit handling or forbid NaN. - **Inclusivity ambiguity:** document `[ ]` vs `[ )` — a silent off-by-one at a boundary is a classic bug. - **Mutability / thread safety:** if `start`/`end` can change, `contains` can race. Prefer **immutable** value types (`data class` / `val`). - **Empty/inverted intervals:** decide and document whether `start > end` is empty or invalid. ## Why it matters Exposing `in` on `VersionRange`, `IpRange`, or `TimeWindow` makes call sites read like domain language (`request.ip in allowedBlock`). The API contract — inclusivity, ordering, immutability — is what keeps it correct under real data.
- Why prefer `compareValuesBy` over subtracting fields in `compareTo`?Subtraction of Int fields can overflow (e.g. large-minus-negative), producing a wrong sign. `compareValuesBy` compares each selector safely and lexicographically.
- When can't you use `ClosedRange` for membership?When you need a half-open `[start, end)` interval or non-standard inclusivity/wrap-around — `ClosedRange` is always inclusive on both ends, so you define your own `contains`.
- Why prefer immutable interval types?A mutable start/end makes `contains` non-thread-safe and lets membership results change unexpectedly between calls; immutability gives stable, race-free semantics.
Implementing contains is like writing the membership rule for your own club: define exactly who's inside the velvet rope, including the edge cases at the door.
saying these in an interview costs you the question
- Implementing `compareTo` with field subtraction that can overflow.
- Using `ClosedRange` (always inclusive) where a half-open interval is required.
- Ignoring NaN or non-total ordering, making membership undefined.
- Exposing a mutable interval and assuming `contains` is thread-safe.
- Leaving inclusivity (`[ ]` vs `[ )`) undocumented.