-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Another proposal for a standardized object construction/initialization. #48
Comments
While I like the approach a question to consider: |
Well, the compiler is free to perform copy elision. This might be a good time to put some effort into implementing it. |
This is probably a terrible idea but: type Foo = object
tbl: Table[int, string] {.init.}
proc `=init`[K, V](tbl: var Table[K, V]) =
tbl = initTable[K, V]()
var foo: Foo
foo.tbl["key"] = "value" |
currently, generics, varargs, aliases, and statics are fragile. perhaps they are less explored, or because they are buggy so less people use/fix them. |
I would add distinct and ranges to that list. Regarding the proposal, I really like it. That would bring New can also be refined the same way (except for the finalizer): proc new(f: var Foo, arg1: Bar, arg2: Baz) =
f.field1 = arg1
f.field2 = arg2
proc new(T: typedesc, args: varargs[typed]): T =
new result # what to do with the finalizer here?)
new(result, args) So that we can do |
Ouch, I would only list 'statics' here... :-) |
@Araq Agreed :-) |
Proposal 1 is clearly superior in my book as it doesn't require yet another language feature. (Btw it needs to use |
I should clarify that proposal 2 is an extension of proposal 1, not an alternative. It's intended to make the language a bit more convenient after Proposal 1 is in place. |
Any proposal that transforms |
This goes back to some of our old discussions about constructors. In Nim, every single proc can be considered a constructor, so it's a problem that the compiler is not able to check that all the fields of the result are being initialized. And we'll probably need some kind of control-flow-aware analysis to do this properly. If we have such control-flow based analysis, I can imagine the In any case, before turning this down, we'll have to come up with an alternative. To achieve equal convenience in all the examples I have provided, I can imagine two types of schemes:
template init(x: var T, args: varargs[typed]) =
x = init(T, args)
proc init(T: type, x, y: ...): T =
result.x = x
result.y = y As you can see, this rewrite is roughly equivalent, it doesn't suffer from the |
Yes, good.
This should be solved by embracing the object construction syntax |
@Araq The problem with this approach is that whenever constructing one of this fields requires some long and complicated code, people will move this code above the definition of Foo, and then assigning to a field may trigger a copy. In Rust, this does not happen because their control flow analysis guarantees that no copy will happen. let foo = Foo(x: somethingWhichIsComplicatedToWriteInline())
# becomes
let x = somethingWhichIsComplicatedToWriteInline()
let foo = Foo(x: x) By the way, forcing this pattern makes |
It gets turned into a move in Nim too.
No,
That never was its purpose.
No, |
So, are we happy enough with the revised proposal? I can edit the spec at the top of this issue. |
Yeah please do so. Not sure what the revised proposal now looks like. ;-) |
Great to know, I was not sure about this!
Well, yes, but hardly more convient than In my experience, This achieved most of the benefits of immutability, with some convenience: for many kind of objects, the only moment you want to mutate them is at construction time, and the use of the special variable Maybe this was never the intended use of |
It's exactly as convenient as your use case which uses result for construction.
That's all still true when construction turns from
It isn't "cumbersome" in my experience. shrug |
I've updated the proposal. Only points 1) and 2) of Proposal 1 have been changed. |
I think I failed to highlight my point. The special variable If the plan is to disallow this pattern, I don't think that
Exactly. The point is that having The following pattern type Foo = object
a, b, c: int
proc makeFoo(x: int): Foo =
result.a = x
result.b = x + 1
result.c = x * x
proc useFoo() =
let foo = makeFoo(5)
echo foo allows to avoid If this pattern cannot be used, what is the advantage of having the |
No, but I completely disagree with your point.
And again, every time you use some kind of accumulation to compute the 'result', result does carry its weight. It was never its primary purpose to allow some "nice" unstructured object initialization. |
Ok, sorry, maybe it was not its use, but I had misunderstood it for one of its main use cases :-) Guaranteed "named return value" optimization is a much better reason for the existence of |
What's the status of this? having a smarter |
just had a bit of discussion about this with @zah - one important point this proposal does not address is the special status that constructors have in C++: they allow you to limit construction to a specific set of functions per type, meaning you can be guaranteed that no "rogue" instances will be created. With nim gaining destructors, having that kind of control over construction is crucial in order to be able to write efficient destructors - you can make assumptions about what states the object can be in - it brings balance to construction and destruction - one function is guaranteed to be called when instances spring to life, and another when they pass. |
This deserves a more elaborate answer but in a nutshell: Constructors that require parameters beyond the
|
disallow, initially at least - this means the resize operation is not callable, but the add operation is. later, one can add a special factory-based resize etc. basically, offer the functionality that is meaningful in a context where construction is limited, but disallow the rest.
only self - by default, parameters, optionally? it's actually an interesting feature - to have the ability to get a compiler error whenever an instance is projected to go out of scope - that means you can force the user to explicitly think about destruction - not a bad idea actually - similar to
this is not sufficient - the constructor is used in
I wonder if this can be enforced in some other way however - ie special annotations, tricks with types and concepts etc |
I don't understand how this and your following examples differ from my description. |
let's say that I want |
That idea came up before and it was suggested the type could be annotated with |
@arnetheduck with current Nim your objects always start zeroed out. That is actually a quite nice property to have. Introducing constructors that allow what you want would destroy this property. |
So, why aren't we making progress with this proposal? I've already included the suggested template init*[T](val: var T, args: varargs[typed]) =
mixin init
val = init(T, args) |
yes - that is exactly what I'm asking for tools to avoid. as a library author, I would like to create beautiful libraries where the defaults are meaningful for the type that I create, and not an arbitrary choice made by the language. I think Nim recognizes how important it is - in fact it is so important that a massively backwards incompatible change was introduced in the language to special-case two types: strings and sequences - these two behave like is natural for their type - there is now an empty string and not
@zah - edit: the proposed function replaces instance - this is not an issue (thanks @zah for pointing out) |
the main example is another great case for why you would want this: to meaningfully use the right now for example, you can put things into a
very hard to correlate this issue / bug to "you forgot to call initTable" - there's just nothing to help you ("read the docs" aka "blame the user" is not a helpful option). |
@arnetheduck, your statement is not true. Take a look at the definition carefully. It assigns a fresh copy to the memory location which should be always safe. |
Otherwise, my proposal can be extended further to cover "default construction" too. But with the current destructors and move handling plans, Nim needs to assume that all-zero locations are always safe to use. The main reason for this is that an object may be moved out from an array, which have to zero out the array location in order to defuse the destructor that will be called when the array is destroyed. The only possible alternative to this would be allocating a bitmask with every array/seq specifying which slots have been already destroyed. This solution has been considered too expensive by Araq. Well, to clarify, you need to |
@PMunch just pointed out another interesting case:
clearly, 0 is not an expected value in this case.. |
Currently, the approach of initializing object used in the standard library is to offer a proc following a naming convention built around the type name (e.g.
initTable[string, int]()
). This has the following drawbacks:1. You cannot write generic code that constructs an arbitrary type given as a parameter.
This is the most significant drawback and it should be obvious.
2. There is an inconvenient DRY violation when initializing fields of record types
3. Another DRY violation when using type aliases
Proposal 1
All of the above problems can be solved by switching to the following convention:
EDIT: After some discussions in the comments below (https://github.com/nim-lang/Nim/issues/7832#issuecomment-403488670), I'm revising the proposal:
init
proc for a type is defined like this:init
template can be used to initialize existing locations by inferring their type:Previous Proposal
1) The canonical `init` proc for a type is defined like this:init
template accepting a type name and forwarding the initialization parametersThe previously problematic code is now beautiful:
Proposal 2
Make
init
a well-known proc similar toitems
andpairs
and treat the currently invalid expressions such asFoo(x, y, z)
asinit(Foo, x, y, z)
(Foo
here is assumed to be a type).Some further notes:
Nim is gradually moving to user-defined memory management schemes. The initialization scheme can be augmented in the future to play well with various memory allocation schemes. Consider the following helper:
For supporting arenas and other similar schemes, it would be easy to extend this syntax with additional niceties such as the following:
The text was updated successfully, but these errors were encountered: