Hello everyone! I am a new Nim user (and, I have to say, I am pretty excited about this language!) coming from Julia. I was tesing some of Nim's capabilities when coming to generic types and procs, when I ran into this code not compiling:
type
AbstractType = int or string
TestType*[T : AbstractType, Y : AbstractType] = object
a : T
b : Y
proc newTestType[T : AbstractType, Y : AbstractType](a : T, b : Y) : TestType[T, Y] =
return TestType[T, Y](a : a, b : b)
proc print_test_type(t : TestType) =
echo t.a
echo t.b
let
t1 = newTestType(1, 2)
t2 = newTestType("string", "here")
t3 = newTestType(10, "mixed")
echo typedesc(t1)
print_test_type(t1)
echo typedesc(t2)
print_test_type(t2)
echo typedesc(t3)
print_test_type(t3)
The compiler error is this:
/home/francesco/Sources/nim/GenericTypes.nim(44, 21) template/generic instantiation of `newTestType` from here
/home/francesco/Sources/nim/GenericTypes.nim(35, 20) Error: cannot instantiate TestType
got: <type int, type string>
but expected: <T: AbstractType, Y: AbstractType>
On the other hand, if I substitute
TestType*[T : AbstractType, Y : AbstractType]
with TestType*[T : AbstractType, Y : int or string]
, the code correctly compiles and executes, resulting in the expected result:
[system.int, system.int]
1
2
TestType[system.string, system.string]
string
here
TestType[system.int, system.string]
10
mixed
Does anyone know why this is the case?
coming from Julia.
Have missed that statement earlier this morning.
So I wonder if you really intent what you are doing?
For your TestType, you are not defining one single type, but four distinct different types.
So maybe what you want is sum types, called object variants in Nim?
For your compile problem, my guess is that for
TestType*[T : AbstractType, Y : AbstractType]
the compiler assumes that T and Y are identical, but you want then not identical, so the explicit "or types" compiles.
Hello,
Redefining TestType this way allows reusing AbstractType, but I find it a little repetitive:
type
TestType*[T : distinct AbstractType | AbstractType; Y : distinct AbstractType | AbstractType] = object
a : T
b : Y
Also, welcome to Nim, @vitreo12 !
It's actually enough to change just one of the type constraints to make it work:
type
TestType*[T : AbstractType; Y : distinct AbstractType | AbstractType] = object
a : T
b : Y
That's nice, but the fact that the signature of newTestType doesn't have to be changed shows that either there's still something really wrong with Nim's type resolution or that it works in a way which so runs counter my intuition that it's not for me: the generic type parameters of newTestType are not distinct, hence AbstractType is bind once, so why does
t3 = newTestType(10, "mixed")
even compile?the generic type parameters of newTestType are not distinct
They are unified at the type level in TestType. The (inferred) type is still int or string . Within newTestType , terms become available and obviously, type resolution happens at the term level. If this is intended or not might to be seen.
Welcome to Nim.
You are actually touching an advanced part of the Nim type system, the bind once versus bind many part.
bind once vs bind many
Let's go through that with an example.
proc foo1(x, y: SomeInteger) =
echo "Ran foo"
proc foo2(x: SomeInteger, y: SomeInteger) =
echo "Ran foo"
proc bar[T, U: SomeInteger](x: T, y: U) =
echo "Ran bar"
proc baz(x, y: distinct SomeInteger) =
echo "Ran bar"
let a = int32 10
let b = int64 20
block: # fails
foo1(a, b)
block: # fails
foo2(a, b)
block: # works
bar(a, b)
block: # works
baz(a, b)
In the first case foo1, from the signature it is intuitive that x and y should be of the same type.
The second case foo2 is equivalent to foo1, for generic instantiation, once SomeInteger has been inferred from the first parameter, it is applied verywhere else. It is called bind once. The main benefit is that both definition are interchangeable, and it seems to be a good default.
There are 2 ways to ask the compiler to not bind once.
Option 1 via indirection: in the bar function, x of type T which is instantiated to int32, and y of type U which is instantiated to int64. The [T, U: SomeInteger] declaration only restrict T and U to be of SomeInteger but never instantiates SomeInteger so it's not bind to any concrete type like in the 2 previous examples.
Option 2 via explecitly asking bind many behaviour: in the baz function, the distinct keyword tells the compiler to lock the inference of SomeInteger to the type of x.
The tricky part 1: distinct solution
Hopefully the examples are simple enough and you managed to follow me until here. The key part to remember is that distinct tells the compiler to not bind once.
You might think, oh I just need to use distinct for my types then.
type
AbstractType = int or string
TestType*[T, Y : distinct AbstractType] = object
Unfortunately, distinct has a completely different meaning in a type section.
``distinct`` types
Distinct types are well covered in the manual. It allows you to create units (for example physics units like Meter and Mile or currency like Dollar and Euro) and prevent you from mixing them at the type system level.
Example
type
Dollar = distinct int
Euro = distinct int
# Let's borrow procedures from the base ``int`` type
proc `+`(x, y: Dollar): Dollar {.borrow.}
proc `+`(x, y: Euro): Euro {.borrow.}
# And overload the print to add the unit
proc `$`(x: Dollar): string =
$int(x) & " dollars"
proc `$`(x: Euro): string =
$int(x) & " euro"
let a = 10.Dollar
let b = 3.Dollar
let c = 5.Euro
let d = 1.Euro
echo a+b # Prints 13 dollars
echo c+d # Prints 6 euros
# Compile-time error "Type mismatch"
# echo a+c
The tricky part 2: indirection solution
Since distinct doesn't work in a type section. You might think, well I can use the bar solution, bind T and U to different types.
type
AbstractType = int or string
TestType*[T: AbstractType; Y: AbstractType] = object
a : T
b : Y
proc newTestType[T, Y: AbstractType](a: T, b: Y) : TestType[T, Y] =
return TestType[T, Y](a : a, b : b)
proc print_test_type(t : TestType) =
echo t.a
echo t.b
let
t1 = newTestType(1, 2)
t2 = newTestType("string", "here")
t3 = newTestType(10, "mixed")
echo typedesc(t1)
print_test_type(t1)
echo typedesc(t2)
print_test_type(t2)
echo typedesc(t3)
print_test_type(t3)
Well unfortunately it doesn't work, within a type section, all the generic T, Y types are bind once.
The solution
@thenjip nailed it, you need to shake the typesystem a bit until you "bind many indirections" directly at the type section level instead of the proc level:
type
AbstractType = int or string
TestType*[T : distinct AbstractType | AbstractType; Y : distinct AbstractType | AbstractType] = object
a : T
b : Y
proc newTestType[T, Y](a: T, b: Y) : TestType[T, Y] =
return TestType[T, Y](a : a, b : b)
proc print_test_type(t : TestType) =
echo t.a
echo t.b
let
t1 = newTestType(1, 2)
t2 = newTestType("string", "here")
t3 = newTestType(10, "mixed")
echo typedesc(t1)
print_test_type(t1)
echo typedesc(t2)
print_test_type(t2)
echo typedesc(t3)
print_test_type(t3)
Language design
More for future improvement of the language, I think the distinct keyword to create unit types and the distinct keyword for the bind many type inferrence should be separated to improve language ergonomics. We could use mixed instead for the bind many
proc baz(x, y: mixed SomeInteger) =
echo "Ran bar"
Thank you all very much for the welcome and for all the help! I know see where the problem is, and I hope that, in the future, an easier syntax would work.
In any case, I went with the following solution, using a generic type at the type level and specializing only in the procs' interface:
type
AbstractType = int or string
#Fully generic type, but constrained in the procs interface.
TestType*[T, Y] = object
a : T
b : Y
proc newTestType[T : AbstractType, Y : AbstractType](a : T, b : Y) : TestType[T, Y] =
return TestType[T, Y](a : a, b : b)
proc print_test_type(t : TestType) =
echo t.a
echo t.b
let
t1 = newTestType(1, 2)
t2 = newTestType("string", "here")
t3 = newTestType(10, "mixed")
echo typedesc(t1)
print_test_type(t1)
echo typedesc(t2)
print_test_type(t2)
echo typedesc(t3)
print_test_type(t3)
What do you think of this solution? The only problem here is that, to construct a
TestType
one should always use the newTestType
proc in order to have the correct AbstractType
behaviour.I think it is fine since TestType's members are private.
So, constructing a TestType from another module would require a call to newTestType anyway.
What do you think of this solution?
I think it's a good solution.