Sometimes it is useful to mark a property that isn't an enum case as indirect, such as when an object needs to store an instance of itself sometimes:
struct Tree<Node> {
var node: Node
var nextSibling: Tree? // error: struct `Tree` cannot store instance of itself
var firstChild: Tree? // error: struct `Tree` cannot store instance of itself
}
We could implement an @Indirect property wrapper for these use-cases.
@propertyWrapper public enum Indirect<Wrapped> {
indirect case wrapped(Wrapped)
public var wrappedValue: Wrapped {
get {
switch self { case let .wrapped(wrapped): return wrapped }
}
set {
self = .wrapped(newValue)
}
}
}
And then the tree example from above works well.
struct Tree<Node> {
var node: Node
@Indirect
var nextSibling: Tree?
@Indirect
var firstChild: Tree?
}
Alternatively we could allow indirect on properties, but that would require more work.
This topic has been discussed quite a bit previously on the forums. Please see some of the previous discussion, particularly this comment describing some of the performance characteristics and why a language-supported indirect modifier on structs would be preferable to a property-wrapper-based approach.
To be fair, accessor macros and inout pattern matching might change the calculus a bit, since a macro can conceivably be attached to a struct and pick out the indirect properties within it, placing them in a common box, and if we're able to pattern-match inout bindings into enums, then you could get the same efficient box representation as enums do by using an indirect enum as the box type. Macros wouldn't be able to directly integrate with reflection codegen yet, but could get pretty close to what I think an ideal language-integrated implementation would look like otherwise.
True, and if we were in a vacuum where indirect enum/case didn't exist, that sounds like it would be a reasonable implementation. But for consistency with the feature we already have, would we still want it to be a language-supported feature?
We could say that structs are different enough from enums (because enums don't give you much of any control at all over their storage) that it warrants a different API shape, but that still feels uncomfortable when trying to educate users on why the two things exist.
But for folks who want something that they can do in their own code base in the meantime, the macro pair would work.
Right, one can use a similar combination of member/member-attribute/accessor macros to what Observable does to move the storage of mutable properties into a separate type. For example, this:
@Indirect
struct Tree<Node> {
var node: Node
var nextSibling: Tree?
var firstChild: Tree?
}
could turn into, e.g.,
struct Tree<Node> {
class Storage {
var node: Node
var nextSibling: Tree?
var firstChild: Tree?
}
var storage: Storage
mutating func makeUnique() { /* if storage is not uniquely owned, make it so */ }
var node: Node {
get { storage.node }
set {
makeUnique()
storage.node = newValue
}
var nextSibling: Tree? { ... }
var firstChild: Tree? { ... }
}
I am very curious how will this macro-based approach will work out.
I'm assuming that one wants all of the indirect properties to be allocated together in a single box, rather than putting them in separate boxes. You need the storage to be at the type level for that to work.
In some cases, you might not want some properties to have to suffer the additional indirection of going through storage. Continuing with the Observation analogy, I suppose we could offer a no-op macro like @IndirectIgnored that would keep a field in inline storage.
That would at least be consistent with indirect enum, where the indirectness is applied to all the cases' payloads (without the ability to opt out individually).
That has the drawback of making it slightly more work to create an @Indirect struct where only a small subset of the fields are indirect. The other option would be to require @Indirect on the fields that you want. Then maybe you could allow @Indirect(ALL_THE_THINGS) as the type-level macro to get the shortcut.
EDIT: Maybe that's not even necessary. If @Indirect is on the type and none of the fields are tagged, we could assume the user wants all of them to be indirect.
There's an interesting balance here between "make it easy" and "make it performant" by default.
This is more implementation detail. I'm more curious about about semantic correctness. I feel like more granular control over indirectness might be preferable, but I don't really have a strong opinion (that's why I ask, not argue).
If we're discussing an approach via macros, one could implement per-property indirectness using the same tactic as ObservationTracked/ObservationIgnored in Observable - marker attributes.
@IndirectSupporting
struct Foo {
var a: Int
@Indirect var b: Foo
}
Forgive my ignorance, but why can't (or shouldn't) classes be used in this case, instead of structs?
Indirect enums are an interesting case insofar as regular enums are classic value types yet indirect enums behave like classes, but they are still distinct from classes in how they can be used. The distinction is less clear between classes and hypothetical indirect structs.
I would guess that most other struct vs class distinctions would still apply:
Indirect structs wouldn’t support inheritance or deinitializers.
Assigning an instance of an indirect struct to a variable would create a copy, not another reference to the same instance.
If you pass an instance of an indirect struct as the argument of a function (and the function parameter is not inout), you can be certain that the instance will not be mutated.
Indirect structs would get a synthesized memberwise initializer.
The more I think about it, the more I doubt which approach is better. Fundamentally, indirectness refers to how we access the value: directly or via a pointer. Therefore, it has nothing to do with the type itself.
The indirect property of struct variant provides precise control over which properties will be accessed via dereferencing.
However, the indirect struct variant has its own advantages:
The underlying implementation will differ for copyable and non-copyable types. Copyable types should be wrapped by a transparent COW box, while non-copyable types shouldn't be ref-counted objects at all. It might be easier to handle this at the type declaration site, especially in generic contexts.
Both copyable and non-copyable structs can have deinit supported (COW boxes are subject to ARC, and non-copyable structs have deinits due to linearity). It's debatable whether allowing deinits in this case is a good idea, but it is technically possible.
In my mind, an @Indirect struct should provide copy-on-write semantics. Essentially, @Indirect should change the implementation mechanism for the struct without changing its semantics.
I don’t want to lose sight of the fact that inline vs indirect representations are an important, if incomplete, part of the language semantics today, reflected in the definition of classes and the existence of indirect enums. I would rather see that story fleshed out at the language level rather than being split between abstraction levels.
I do think having indirect struct and indirect vars in the language is the right endpoint, but macros are a more accessible way of prototyping and experimenting with an implementation.
Yes, because you're not always going to want to indirect the entire storage of the struct, so being able to control which fields get put in a box and which remain inline is important.
Would all of a type’s indirectvars share a single external storage and refcount? Otherwise I don’t see much of an advantage over wrapping those vars in a stdlib-provided indirect struct Box<T>.