Hi,
sorry if this is a newbie question.
My understanding is that int is both a type and a type class:
Furthermore, every generic type automatically creates a type class of the same name that will match any instantiation of the generic type.
int - the generic signed integer type; its size is platform dependent
Also,
A type class can be used directly as the parameter's type. Procedures utilizing type classes in such manner are considered to be implicitly generic.
I was wondering if (on a 64bit platform)
proc xyz(n : int) = ...
would be interpreted as
proc xyz(n : int64) = ...
or
proc xyz[T : int](n: T) = ...
On investigation, it appears that neither is correct. Instead,
let n : int64 = 1
proc test[T : int](n : T) = ...
test(n)
will not work. To create a generic procedure for all int types one has to use
proc test[T : int64](n : T) = ...
instead.
proc test[T : int64](n : T) = ...
proc test[T : int16] (n : T) = ...
will fail with an "Error: ambiguous call"
proc test[T : int64 | int8](n : T) = ...
proc test[T : int16 | int32] (n : T) = ...
works as expected.
proc test(x : float64) = ...
proc test(x : float) = ...
will fail with an "Error: redefinition of 'test'; ...".
Is my understanding correct? If so, what is the reasoning for the design choices that result (to my mind) counter-intuitive program behaviour?
Thanks for your help.
Another odd example:
proc test(n : int) = echo "could be negative"
proc test(n : uint) = echo "must be positive"
test(1)
and
type
I = int | int8 | int16 | int32
UI = uint | uint8 | uint16 | uint32
proc test(n : I) = echo "could be negative"
proc test(n : UI) = echo "must be positive"
let i = 1
test(i)
work as expected.
But
type
I = int | int8 | int16 | int32
UI = uint | uint8 | uint16 | uint32
proc test(n : I) = echo "could be negative"
proc test(n : UI) = echo "must be positive"
test(1)
returns "Error: ambiguous call; both test5.test(n: I) ... and test5.test(n: UI) ... match for: (int literal(1))"
Of course, the compiler behaves as described in "Overloading resolution" in the manual - but does it make sense?
int - the generic signed integer type; its size is platform dependent
"generic" means something like "vanilla" here, not "generic type". The choice of words is a newbie trap.
As to your results:
the int refers to specific type rather than a type class
Yes, see above.
the type int is distinct from int64, even on a 64bit platform
int64 is not implicitely convertible to int
Yes, for a reason: int is designed to be platform-dependent, int64 is not. The compiler treating them as equivalent would be a bad idea. Even types which are identical on every platform can be distinct, we even have a keyword for it: type myInt = distinct int. Nim generally doesn't do many automatic type conversions out of the box, that's intentional.
the type class int does not include the type int64 , i.e.
It's not a type class, see above
Thanks for the answers
As to the answer to
the int refers to specific type rather than a type class
Yes
I do not believe that it is quite as straight-forward in Nim. I have given some examples in https://github.com/nim-lang/Nim/issues/12552#issue-514013349
As to the answer to
int64 is not implicitely convertible to int
Yes, for a reason: int is designed to be platform-dependent, int64 is not. The compiler treating them as equivalent would be a bad idea. ... Nim generally doesn't do many automatic type conversions out of the box, that's intentional.
I do understand the point, and I certainly would not want the compiler to treat them as equivalent. However, I do expect the compiler to convert int64's to int's on 64bit platforms, as this is safe - it does not involve any narrowing.
If the compiler complains on a 32bit platform - so be it. At least the program fails at compile time. Anyway - I may never try to run the program on a 32bit platform. This seems better than have users blindly (or out of convenience) use the int type for very large integers and find the errors of their ways at runtime on 32 bit systems.
int32 conversion to float64 is also safe as there is no information loss. But Nim chooses to be explicit by default as working around implicit (especially conversion) is a pain.
You can always do this:
converter lenientInt64toInt*(x: int64): int =
when sizeof(int) == 8:
{.error: "int64 to int conversion is unsafe on 32-bit platforms.}
else:
int(x)
And now you can import it everywhere this helps.
Thanks
This still leaves the question: how can I avoid implementing each int procedure for int64 also,
i.e. how can I ensure that the integer version of double is called for i without uncommenting the int64 version?
converter lenientInt64toInt*(x: int64): int =
when sizeof(int) == 4:
{.error: "int64 to int conversion is unsafe on 32-bit platforms".}
else:
int(x)
proc double[T](x : T) : T = x
proc double(n : int) : int = 2 * n
#proc double(n : int64) : int64 = 2 * n
let i = 1'i64
echo double(i) # outputs 1
You could make some specialized types or exploit some of the combined types that Nim already defines. For example:
proc double[T: SomeFloat](x : T) : T = x
proc double[T: SomeInteger](x : T) : T = 2 * x
echo double(1.0)
echo double(1)
echo double(1'i64)
SomeInteger is defined in system.nim (https://github.com/nim-lang/Nim/blob/devel/lib/system.nim) as:
SomeSignedInt* = int|int8|int16|int32|int64
## Type class matching all signed integer types.
SomeUnsignedInt* = uint|uint8|uint16|uint32|uint64
## Type class matching all unsigned integer types.
SomeInteger* = SomeSignedInt|SomeUnsignedInt
## Type class matching all integer types.
That works for your example.
It does not work if I want to treat int's and uints differently: (nb: SomeSignedInt and SomeUnsignedInt are non-overlapping)
proc double[T: SomeUnsignedInt](x : T) : T = x
proc double[T: SomeSignedInt](x : T) : T = 2 * x
echo double(1) # Error: ambiguous call
It does not work if I want to treat int's and other numbers differently: (nb: SomeSignedInt is a strict subset of SomeNumber)
proc double[T: SomeNumber](x : T) : T = x
proc double[T: SomeSignedInt](x : T) : T = 2 * x
echo double(1) # Error: ambiguous call
It does not work if I want treat small int's differently:
proc double[T: int8|int16](x : T) : T = x
proc double[T: SomeSignedInt](x : T) : T = 2 * x
echo double(1) # Error: ambiguous call
even if I try :
proc double[T: int8|int16](x : T) : T = x
proc double[T: int|int32|int64](x : T) : T = 2 * x
echo double(1) # Error: ambiguous call
Seems like you could just elaborate as needed:
proc double[T: SomeSignedInt](x: T): T =
when (T is int) or (T is int64) or (T is int32):
2 * x
else:
x
echo "int double(1): ", double(1)
echo "int64 double(1): ", double(1'i64)
echo "int32 double(1): ", double(1'i32)
echo "int16 double(1): ", double(1'i16)
echo "int8 double(1): ", double(1'i8)
I realise that it is easy to write a procedure that works for any combination of types that I need to deal with.
The problem that I have is this:
How do you write a generic procedure for all signed integer types that does not break if somebody at a later date decides to overload it with either a more general or a more specific procedure?
To me, that is a fairly basic question for any serious programming language.
I'm a Nim newbie, so someone else can chime in here but the rules for overloading resolution are detailed here -- https://nim-lang.org/docs/manual.html#overloading-resolution.
For purposes of illustration can you illustrate how you'd solve this is another "serious programming language" that allows overloading? I know Haskell doesn't allow function overloading (though I wouldn't be surprised if there was an extension to do so). I seem to recall that Idris does but I've not had a chance to dive into Idris in any depth.
Generics have the same priority on overload meaning:
proc foo[T](n: T) = discard
proc foo[T: int](n: T) = discard
will both lead ambiguous calls even if you pass an int because they are generic.
The following will not:
proc foo[T](n: T) = discard
proc foo[T](n: int) = discard
Regarding conflicts, you have 2 solutions:
I don't buy the argument of serious programming language, both C and Go are serious languages without generics at all.