I would like to use it more--I've seen it brought up as a killer feature--but I'm having some trouble understanding the borders of this little kingdom in Nim.
Specifically I don't understand how/when/to what degree range[a..b] acts like a "real" type (like int8) and when it doesn't. I also do not have a clear image of when/how/if it is preserved through generics and meta-programming (see below). I don't understand why it isn't a Nim type class like enum, even though it almost, kind-of is (ibid).
Here are some smaller examples of some explorations I did, trying to learn, but at each step I had trouble finding consistency; each block below has an implicit question of "is this undefined behavior or is it desired/designed to be just-so?"
type SmallNat = range[0..9]
static:
# This erases the `range[a..b]` and happily assigns values outside of the range.
proc iwith1(v: var SomeOrdinal, n: int): auto =
if n >= int(v.low) and n <= int(v.high):
v = typeof(v)(n)
var x: SmallNat = 5
x.iwith1(1_000_000_000)
assert x is SmallNat and x >= 10 # What does this even mean?
static:
# This is the same, even though we're allowed to role-play as if `range` is a "type class" so long as we use a sum type.
proc iwith2(v: range | void, n: int): typeof(v) =
if n >= int(v.low) and n <= int(v.high): typeof(v)(n) else: v
const q = SmallNat(1).iwith2(1_000_000_000)
assert q isnot SmallNat and q is int and q >= 10
static:
# I'm not sure what `range` does or what it is supposed to mean in the sum type, because `range` is
# explicitly not a type class.
assert not (compiles do:
# Error: invalid type: 'range' in this context: 'proc (x: range)' for proc
proc rbar(x: range) =
discard)
static:
# Aside: the same confusing drunk-type-class semantics is found with `Ordinal`:
assert compiles do:
proc ofoo(x: Ordinal | float) =
discard
ofoo(1) # yup
ofoo(littleEndian) # yup
ofoo(1.2) # yup
assert not (compiles do:
# Error: invalid type: 'None' in this context: 'proc (x: Ordinal)' for proc
# ^---- much more confusing error message though.
proc obar(x: Ordinal) =
discard)
static:
# Despite the aforementioned procs which erase the range, it *is* preserved when we use explicit
# generics, even though I had the delusion this function should be semantically equivalent to `iwith1`?
proc iwith3[T: SomeOrdinal](v: var T, n: int): auto =
static: assert T is SomeOrdinal and T is range
if n >= int(v.low) and n <= int(v.high):
v = typeof(v)(n)
var y: SmallNat = 3
y.iwith3(1_000_000_000)
assert y is SmallNat and y == 3
static:
# We're also allowed to say `T: range`, again role-playing as a type class, but now it seems
# to...work?
proc iwith4[Q: range](v: Q, n: int): auto =
static: assert Q is range
if n >= int(v.low) and n <= int(v.high): typeof(v)(n) else: v
assert SmallNat(4).iwith4(1_000_000_000) == 4
assert not compiles(9.iwith4(1_000_000_000)) # it does demand a real, living range[], not an int
# Finally, this is actually closer to what I first tried/hoped would work,
# assuming A, B would be inferred:
proc iwith5[A, B: static[int]](v: range[A..B], n: int): auto =
if n >= A and n <= B: range[A..B](n) else: v
when not compiles(iwith5(range[1..12](7), 11)):
static: echo ":("
# It is however a rather strange function that seems to "trick" compiles():
when (compiles do: assert iwith5[1,12](7, 1100) == 7):
static: echo "LIES"
# Error: conversion from int literal(7) to range <invalid value>..<invalid value>(int) is invalid
# assert iwith5[1,12](7, 1100) == 7
# Also, `range[]` seems to be completely ignored by `static[]':
proc stat0(n: static[range[0..5]]): string =
when n > 5:
return "why"
else:
return $NimMajor
proc stat1[n: static[SmallNat], T](xs: array[n, T]): int =
return xs.len
proc stat2[n: static[range[0..0]], T](xs: array[n, T]): int =
return xs.len
proc stat3[n: range[1..4], T](xs: array[n, T]): int = # probably makes no semantic sense, but it compiles?
return xs.len
static:
assert stat0(999) == "why"
assert stat1([1,1,1,1,1,1,1,1,1,1,1]) == 11
assert stat2([1,1,1,1,1,1,1,1,1,1,1,0]) == 12
assert stat3([1,1,1,1,1,1,1,1,1,1,1,2,2]) == 13
Just briefly looking at these, most of them seem to be bugs in likely caused by range checks not being emitted inside implicit generic code.
proc stat0(n: static[range[0..5]]): string seems to be caused by an issue with implicit conversions from literals to static ranges not being checked.
We don't use it as a matter of policy due to the implict / silent Defect raises that it introduces - ie
proc function(v: range[0..10]) = echo v
var x = 11
function(x)
Another way to put it is that it's quite broken compared to other downsizing in supported range, such as going from int64 to int32 etc which requires an explicit conversion.
I think the best thing that can happen is that the range type is phased out to be replaced with something more explicit. It was an interesting experiment but that's about it.
See also https://status-im.github.io/nim-style-guide/language.range.html
I think the best thing that can happen is that the range type is phased out to be replaced with something more explicit.
Given https://github.com/nim-lang/RFCs/issues/493#issuecomment-1321760615 that seems that is how they are supposed to work.
Given https://github.com/nim-lang/RFCs/issues/493#issuecomment-1321760615 that seems that is how they are supposed to work.
maybe, but "fixing" them by making them explicit would break practically all code out there - ie unfortunately, Natural is a range and it's used all over the place - take setLen for example - it is typically called with an int and the language relies on the implicit conversion to raise a defect on negative lengths.