[TODO] Nim should allow nested types
timotheecour opened this issue · comments
(if nested types are already supported, sorry about the noise and please let me know how!)
rationale
- tons of languages support nested types, (eg C++, D, C#, swift, python ...)
- It provides a level of encapsulation
- it avoids polluting the top-level symbol table.
no nested types makes wrapping libraries in other languages problematic
eg:
test.cpp:
struct Foo {
struct Bar {
};
};
struct Bar {
};
test.nim: generated via ~/.nimble/bin/c2nim tests.cpp
```nim
type
Foo* {.bycopy.} = object
Bar* {.bycopy.} = object
Bar* {.bycopy.} = object
which gives invalid Nim file.
These name clashes are unavoidable in large C++ libraries (or other languages), and workarounds seem very costly (eg changing C++ code? custom handling in nim wrapper?)
other problematic example
template<typename T>
struct Foo {
template<typename U>
struct Bar {
};
};
no nested types makes using DSL's problematic, eg protocol buffers:
message FooBar {
}
message Foo {
Bar bar = 1;
message Bar {
int32 a = 1;
}
}
https://github.com/PMunch/protobuf-nim generates types Foo, Foobar (for submessage) and FooBar (top-level), which clashes and produces invalid code.
NOTE: these protobufs could be from external third party libraries over which we have no control
Proposed syntax for nested types in Nim:
type
Foo = object
age: int
type
Bar = object
name: string
Signals = enum
sigQuit = 3, sigAbort = 6
var a:Foo
var b:Foo.Bar
What's wrong with?
type
Bar = object
name*: string
Foo* = object
age*: int
bar*: Bar
var a:Foo
var b:Foo.bar
Notice that Bar is not exported but its fields are (or you can use "accessors" procs)
Now there might be some need on the importcpp side but for pure Nim I think my solution is fine.
TIL you can declare a variable's type using the field of another type. Nice.
-
adding a field
bar
toFoo
changes the ABI, notablyFoo.sizeof
increases byBar.sizeof
. This is not the case in all the other languages that allow nested types, and this pattern completely changes the meaning when porting foreign libraries -
these other languages allow (and use!) allow nested type alias, eg: (in D)
struct Foo(T) {alias This=Foo;}
(useful in more complex use cases); with suggested pattern this would not work
This pattern is very common, for a C++ example see:Member types
in http://en.cppreference.com/w/cpp/container/vector -
this doesn't address name clashes within a module I mentioned in the top-level post
-
this would not work with automatic wrapping of foreign libraries that use nested types ; and would lead to a lot of pain if doing manual wrapping
-
this also wouldn't work with DSL's (eg protocol buffer example I mentioned); the generated types will clash (they'd be generated in the same module)
-
What would be downsides of the syntax I proposed?
-
Side question: why is
var b:Foo.bar
syntax even allowed? that just leads to confusion; insteadvar b:Foo.bar.type
should be required IMO
@timotheecour you are totally right import macros
should not be allowed. But I am against your proposal of subtyping. I don't think that your syntax proposal will be accepted, becaus the syntax is something that probably won't get such drastic changes anymore. But that doesn't mean that a pattern that feels like subtyping won't be possible. You can already get something like subtying in Nim.
type
Foo = object
FooBar = object
template Bar(_: typedesc[Foo]): untyped = FooBar
var a: Foo
var b: Foo.Bar
var c: a.type.Bar
But you would be required to resolve conflicting names manually when wrapping libraries. In this example I did it with prefixing the partent type to the subtype. Then your example with generics, I am afraid I could not get it to work with a template like the template above. But luckyly in practice that is rarely a problem
type
Foo[T] = object
ft: T
FooBar[T,U] = object
bt: T
bu: U
proc buzz[T,U](self: Foo[T], something: U): FooBar[T,U] =
result.bt = self.ft
result.bu = something
var a: Foo[int]
let b = a.buzz
FooBar is not a subtype here. But it solves the same purpose as the subtype, just with less nesting involved. Sometimes you just need to think a little bit different when learning a new language, not everything can be mapped 1:1. Especially subtypes.
adding a field bar to Foo changes the ABI, notably Foo.sizeof increases by Bar.sizeof. This is not the case in all the other languages that allow nested types, and this pattern completely changes the meaning when porting foreign libraries
I am not sure I understand - doesn't your protobuf example embed Bar inside Foo (hence increases Foo's size)?
In any case, I would not introduce such a breaking change at this point, given that its advantages are somewhat limited
Regarding creating type associations involving generic types, the following should work:
import typetraits
type
Foo[T] = object
Bar[T] = object
Baz[T, U] = object
BazAlias[T] = Baz[int, T]
template matchingBar[T](f: type Foo[T]): typedesc = Bar[T]
template unboundBaz(f: type Foo): typedesc = Baz
template partiallyBoundBaz[T](f: type Foo[T]): typedesc = BazAlias[T]
var
x1: Foo[int].matchingBar
x2: Foo[string].unboundBaz[float]
x3: Foo[float].partiallyBoundBaz
echo x1.type.name
echo x2.type.name
echo x3.type.name
The expected output should be:
Bar[int]
Bar[float]
Baz[int,float]
I consider it a bug that it's currently failing. There is an issue with resolving bound types names inside template bodies.
@zah well you did not properly handle the nested type with the generic. Each instance of Foo[T]
has it's onwn instance of all subtypes. Meaning Foo[int].Bar[float]
is a different type than Foo[float].Bar[float]
. Therefore the inner types needs to have two type parameters. Foo[int].Bar[float]
should instanciate FooBar[int,float]
Not gonna happen anytime soon, sorry. We need to focus on stability, not on more features, no matter how good they might look.