Thanks for weighing in here, and sorry that the previous thread fell off my Radar; lots to do!
I won't rehash everything discussed in Codable != Archivable but I think it's a good basis for discussion here.
I am still not convinced that a solution would need to be invented here that doesn't already exist. The same initialization rules that already apply to First
and Second
are no different whether you write the initializer yourself, or implement init(from:)
.
@BigZaphod, in your example, how would you actually write initializers for First
and Second
yourself today? I see a few possible options:
-
Make
parent
optional and patch it up in anaddChild()
method:class Second { unowned var parent: First? } class First { private var children: [Second] = [] func addChild(_ child: Second) { child.parent = self children.append(child) } }
This isn't great as
Second
objects without parents might not be useful, and nowparent
has to be avar
(and exposed) -
Create a
Second
with aFirst
and patch upchildren
:class Second { unowned let parent: First init(_ parent: First) { self.parent = parent self.parent.addChild(self) } } class First { private var children: [Second] = [] func addChild(_ child: Second) { self.children.append(child) } }
This also isn't great:
Second
needs to know to patch up its parent upon initialization, andFirst
needs to exposeaddChild
, which may or may not need to also verify thatchild.parent == self
, which is redundant in many cases -
Create a
Second
inFirst
using a helper method:class Second { unowned let parent: First fileprivate init(_ parent: First) { self.parent = parent } } class First { private var children: [Second] func addChild() -> First { let child = Second(self) children.append(child) return child } }
This is getting better as there's now no way to construct a
Second
from outside of the type, and it doesn't need to know anything about the structure ofFirst
; you can only construct one fromFirst
, which also keeps the structure ofFirst
and relationship between these types consistent. The downside is that you can't create aSecond
on its own and have to go throughFirst
There are a few other isomorphic cases (we can do more fun stuff with visibility), but each of these cases can be represented by an init(from:)
— it largely comes down to: does Second
decode First
and patch up its list of children
, or does First
decode Second
and patch up its parent
reference?
Whatever rule you were using in your existing initializer is the same rule you'd use here. Because the reference is unowned
, I would say that Second
doesn't even necessarily need to encode its parent
, as, well, it doesn't own it! It's up to the parent to own and fix up the child.
So: can you share the real-world use-case here? We can take a look at the existing methods for creating the object graph in the first place, and try to replicate them in init(from:)
. Again, I'd love to be proven wrong here, but haven't yet seen something to convince me that this is broken.
(@QuinceyMorris I didn't get a chance to respond to this, but your final example changes the relationship between X
and Y
to be one-to-one, and also, can't be represented with pure init
s — you have to pull the code out into a static function to hold on to an X
long enough for it to not be deallocated.)
Separately, we have considered adding a post-decode patch-up phase to Codable
(we can add it today with a default implementation that does nothing), but I'm not convinced that this would be necessary to fix this up. Either object A
needs to patch up object B
or vice versa; that can happen in init(from:)
or later. It might be more ergonomic in an awaken
phase or similar, but should be possible in init
.