Codable with references?

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:

  1. Make parent optional and patch it up in an addChild() 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 now parent has to be a var (and exposed)

  2. Create a Second with a First and patch up children:

    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, and First needs to expose addChild, which may or may not need to also verify that child.parent == self, which is redundant in many cases

  3. Create a Second in First 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 of First; you can only construct one from First, which also keeps the structure of First and relationship between these types consistent. The downside is that you can't create a Second on its own and have to go through First

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 inits — 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.

1 Like