-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Allow new
to accept keyword arguments for more control over incomplete initialization
#36789
Comments
Orthogonally to incomplete initialization, just having a form of |
|
How does it complicate the parser? The parser is already capable of parsing keyword arguments elsewhere. Is the
Does this mean that currently the compiler relies on the fact that only a suffix of the fields can be uninitialized? I don't see why the compiler should care about this. The implementation should be pretty straightforward:
Then the logic for accessing fields doesn't need to change at all. We don't have to keep track of which fields are initialized or not. How does the compiler currently "handle" which fields are initialized? I think it just emits the asm to read them whenever they are accessed, right? There shouldn't be anything new to do w.r.t. the current implementation. |
It's probably not a huge complication, but it will certainly complicate some of the front-end-code. The Regarding the compiler, it needs to defensively emit code that checks any ref fields that can be uninitialized to make sure they're not NULL pointers before accessing them. When accessing fields that are always initialized, this doesn't need to be done. Currently, which fields need to be checked and which don't can be tracked with a single number; if it becomes an arbitrary subset, a bitfield will be required instead. I'm not arguing against this feature, just pointing out that it isn't without implementation and maintenance costs. |
I do think this feature makes sense. It's easy to implement for mutable structs, since we can just lower it to a series of |
Now I completely get what you mean. Sorry :) Of course I agree with what you say. I believe the counter you mention is typedef struct _jl_datatype_t {
/* ... */
int32_t ninitialized;
/* ... */
} jl_datatype_t; right?
I also thought about having temporary mutability for immutable structs inside inner constructors, but that seemed somewhat a bigger change to the language, therefore I limited myself to this proposal, which could potentially be implemented in an easier way than the much bigger rework of immutability.
That's also the case that I consider less interesting, because I could just do it myself by using an empty |
FWIW, I've always questioned the need for partially initialized immutable structs. I guess that it can be useful in some situations, but since you can't finish initializing them, it seems like a bit of a dicey feature to me. |
Well, maybe you don't need to "finish initializing" them; they are already finished as they are. They just carry redundant fields for homogeneity with other instances of the same type. I'll reproduce here the example I gave on the forum. Imagine that you want to represent bounded/unbounded open/closed intervals with endpoints of type typedef struct _interval_t {
T left;
T right;
uint8_t kind;
} interval_t; where When you need to represent the interval |
In that case, if |
True, but it isn't straightforward to produce "some value" of type default(::Type{T}) = ...?... for their types if they want to use them in the structure I provide them. Also, either you always return exactly the same instance, or you'll have some allocations. default(::Type{BigInt}) = BigInt(0) would be bad. One has to do
This could be seen as a possible tradeoff once the two approaches are available:
|
@FedericoStra: as I suggested on the forum, I think that Generally, incomplete initialization may be a vestigial feature of Julia since |
@tpapp: You know what? I think if you keep telling me you'll probably convince me in the end! I might be a bit stubborn, but not to the point of being completely unreasonable 😝 So here is my idea: I plan to have a look at |
The main difference between using a union with |
The downside of the struct P
x::Int
y::Int
end and then knowing that in a collection I realize that this is a quite specific usage, so the I would never suggest relying on the "error on access" feature of ref fields. I'm more for the "look before you leap" than the "it’s easier to ask for forgiveness than permission" ideology (when I'm outside of Python). |
Of course do so if you're interested, but I don't think |
What about struct P
xy::Union{Tuple{Int,Int},Tuple{Int,Nothing}}
end with a convenient interface via |
Generally, the more I think about incomplete initialization in the post- If there are examples of incomplete initialization used in the wild, I would appreciate some links. |
Please don't remove incomplete initialization for no reason. There are a lot of applications outside of number crunching, where immutable types are not an option and incomplete initialization and delayed computation of fields is important. Also, |
Just to clarify: I am not (yet) suggesting that incomplete initialization is removed (in any case, this is a breaking change so the earliest this could happen is 2.0), but I think that real-world use cases would still be very interesting. |
Trees and linked-lists? Nodes have to refer to other nodes, but for a list the head and tail don't (yet) refer to anything. Likewise for the root and child nodes of trees. You might think you could circumvent this by using # A demo where the user never sees an object with an uninitialized field, but you still need to be able to construct one transiently
"""
Node{T}(data)
Construct the head of the list.
"""
function Node{T}(data) where T
head = new{T}(data)
head.preceeding = head
head.succeeding = head
return head
end
"""
Node(data, preceeding)
Add a new link after `preceeding`
"""
function Node(data, preceeding::Node{T}) where T
node = new{T}(data, preceeding)
preceeding.succeeding = node
return node
end These are pretty fundamental data structures, and need to be supported. |
Sorry, maybe what I wrote was unclear. My intention is of course to retain typedef struct _jl_datatype_t {
/* ... */
jl_svec_t *types;
jl_svec_t *names;
jl_svec_t *alwaysinit; /* this is the new member */
int32_t ninitialized;
/* ... */
} jl_datatype_t; which is a simple vector of This way |
@timholy: thanks for the example. I agree that julia> mutable struct Incomplete
a
b
Incomplete(b) = (result = new(); result.b = b; result)
end
julia> Incomplete(2)
Incomplete(#undef, 2) If a keyword-based syntax is desired, a macro simple should be able to take care of this just fine. As for immutable |
@tpapp I believe I've already given elsewhere some compelling examples where immutable structs with uninitialized fields can be of use. I'll try to repeat here the main advantage they can provide from my point of view. They allow to work with homogeneous collections of objects with efficient access (no |
In the fullness of time (i.e. for Julia 2.0), I think we should consider eliminating uninitialized fields entirely. This could be done with a combination of union types and field defaults. For example, we could only allow fields to not be explicitly given a value if they have a default and then double down on the convention of using The only hesitation I have about that is that it forces the type of a field to be a union even if the field never escapes the constructor uninitialized. So an intermediate solution would be to somehow enforce that undef fields get defined by the time an object is fully constructed. Of course, that leaves the question of when the object is considered fully constructed. One potential definition is when it is returned from the method in which In any case, I think this is mostly tangential to this issue. None that clashes with this feature that I can see. |
Another avenue to explore is to have an explicit unconstructable token. |
The Problem
Currently, when defining inner constructors, we can decide to incompletely initialize a structure simply by passing to
new(...)
fewer values than the number of fields:Since the call to
new(...)
accepts only positional arguments, this has the obvious limitation that we can only initialize a prefix of all the fields and leave the remaining ones uninitialized. It is impossible to initialize onlyx1
,x42
,x470
andx666
.The Proposal
My proposal is to allow
new(...)
to accept keyword arguments, so that the following code becomes legal:Remarks
Motivation
It is just useful sometimes to be able to do this. See for instance this thread where I asked on the forum if there was a way to achieve this with the current state of the language. There are plenty of workarounds, with more or less significant downsides (complexity-wise and performance-wise), but no direct solution.
Backward compatibility
This should be 💯.
Downsides
I honestly don't see any.
Labels
design, feature, keyword arguments, types and dispatch
The text was updated successfully, but these errors were encountered: