Using `indirect` modifier for `struct` properties

It is a known limitation that structs, being value types, cannot contain properties of the same type. For example, the following code is invalid:

struct Foo {
  var foo: Foo // Error: Value type 'Foo' cannot have a stored property that recursively contains it
}

This code is invalid because in order to create an instance of Foo, we would have to provide an infinite amount of instances. Instantiating it would require infinite memory.

Furthermore, the following code is also invalid:

struct List<T> {
  var value: T
  var next: List<T>? // Error: Value type 'List<T>' cannot have a stored property that recursively contains it
}

While a nil value should be enough to break infinite recursion, this code is invalid because Optional was consciously implemented to not support recursion in favor of yielding higher performance. In Joe Groff's words:

By not marking Optional or its cases indirect, we explicitly made the choice not
to support recursion using Optional because we considered performance to be more
important.
Who benefits from the indirect keyword?

Proposed idea

This proposal would like to allow using indirect modifier on struct properties, like this:

struct List<T> {
  var value: T
  indirect var next: List<T>?
}

Here is a preliminary draft of the proposal, in case there is enough interest for moving it forward:

Thank you,
Eneko

11 Likes

What are some examples of problems best solved with this feature; in other words, how would they be solved now without this feature, and can you discuss why using this feature produces a superior solution?

5 Likes

I think a better approach in this case is simply to use classes instead of structures.
Is there a difference significant enough to justify adding new syntax to the language?

2 Likes

@xwu
There are some examples on Who benefits from the `indirect` keyword? including some exemples of how people fixing using property wrappers, indirect enums, classes or even wrapping up the stored property into an array,

As I mentioned in the other thread, I think it wouldn't add significant improvements to the language since the cases where we would need something like this would be very specific and limited. And also, it is easy to find another solution when we stumble in one of those cases. The way I see it would be just a nice to have.

I'm sorry, I see only code that shows what the proposed feature would do, but I don't see any examples about what problems would be solved using the feature. Would you mind spelling them out for me?

The gist covers some of that, including several alternatives

Sorry, I don't see any in the gist. Can you give some examples here of problems which you've encountered that are best solved using this proposed feature? Feel free to copy and paste from the gist if I'm simply not seeing something.

Sure :)
Here is one I ran into SR-12755
So that's an example where I think the proposed feature could help by allowing to declare those stored members as indirect.

Simply put, this change would allow struct to support recursion, via indirection to the heap. There are several advantages over using classes (eg. structs are value types and copied by value). For example, with recursion we could use struct to build value-typed linked lists, trees or graphs.

This behavior can be achieved with classes, or using structs with property wrappers, boxes or custom optional types (as seen in the gist).

I agree this keyword adds little value to the language. The goal for this pitch is to find out if there would be enough support for it.

3 Likes

Would you be able to solve this problem by using a final class? What disadvantages would you encounter if you used such a design?

1 Like

Classes are passed by reference, and modified without the callee being aware. By using struct, we could guarantee a value passed to a function cannot be modified unless is inout.

That is just one example. There is plenty of documentation on pros/cons of using struct vs class

5 Likes

I'm not asking about the differences between struct versus class.

I'm asking @LucianoPAlmeida to shed some light on how it applies to his specific problem: the question is whether SR-12755 shows an example of a real-world problem best solved by the addition of a new feature, or whether using a final class yields an acceptable or even superior design.

Since he encountered this problem in the course of his work, it is interesting and relevant to consider what he found after exploring various workarounds.

For example, he might say, "I found that a solution using final classes produced the expected behavior but suffered performance penalties in my use case due to x, y, and z." Or he might say, "Although it's usable, the ergonomics suffered because of x, y, and z." Or he might say, "Actually, I tried that alternative and it worked just fine."

2 Likes
  • Cons: accessign the boxed value requires going through .content

This can be fixed by an additional level of property though?

    var _next: Box<List<T>>?  // Recursion 🎉
    var next: List<T>? {
        get {
            _next?.content
        }
        set {
            self._next = newValue.map { Box.init($0) }
        }
    }
2 Likes

Yes, and thus by a property wrapper:

@propertyWrapper
enum Indirect<T> {
  indirect case wrapped(T)
  
  var wrappedValue: T {
    get { switch self { case .wrapped(let x): return x } }
    set { self = .wrapped(newValue) }
  }
}

Usage:

struct Node<T> {
  var value: T
  @Indirect var next: Self?
  
  init(_ value: T, next: Self? = nil) {
    self.value = value
    self._next = .wrapped(next)
  }
}
18 Likes

EDIT: I've become lukewarm to this approach now, since it's a potential performance foot-gun if used heavily, but I'm leaving the text here.

I was a big advocate for indirect for struct properties previously, because this problem came up in SwiftProtobuf; we wanted to represent the generated protobuf types as value types, but there are no restrictions preventing protobuf messages from containing instances of themselves (or worse, can indirectly be recursive), so we had to encode logic in the generator that dictated whether we could generate a simple struct or if we also had to generate private heap-based storage by detecting direct/indirect recursion (among some other conditions).

However, property wrappers now provide an elegant solution to this without requiring changes to the language/compiler. While I do think there's the slight disadvantage that the difference between indirect case and @Indirect for a property could be confusing to new users at first, I think that's outweighed by the principle that (IMO) we should prefer library-based solutions over compiler-based solutions whenever possible.

Now if the pitch became "Should an @Indirect property wrapper like the one above be added to the standard library", I think the answer is definitely yes.

5 Likes

I’m a fan of this idea. indirect should be supported on properties of value type as well as on struct declarations. This is consistent with its usage in enums and brings product and sum types closer to parity in Swift, something which I think is a good goal in general.

In addition to solving the recursive value problem, indirect makes it possible to move storage of a (potentially large) value type to the heap, potentially eliminating a ton of reference counting overhead when copies are made. I don’t think it should be necessary to introduce a user-defined class (which introduces reference semantics into user code) or use a Box / Indirect wrapper to make this performance tradeoff for a type.

The @Indirect property wrapper works ok on individual properties in userland, but I don’t think it’s the right solution for the language itself. It does not allow all values of a type to be stored indirectly, something that is possible with indirect and with a user-defined class. Given that we already have indirect in the language, it seems to me that the most consistent and convenient way to express indirectly stored value-semantics in Swift is to expand the supported uses for the existing feature.

32 Likes

Thanks for the feedback. This pretty much sums my point of view.

@anandabits would you want to add this to the gist? I think it describes well the motivation behind it.

1 Like

No need to use enum for the property wrapper, this is what I had in the gist:

@propertyWrapper
class Indirect<Value> {
    var value: Value

    init(wrappedValue initialValue: Value) {
        value = initialValue
    }

    var wrappedValue: Value {
        get { value }
        set { value = newValue }
    }
}

struct List<T> {
    var value: T
    @Indirect var next: List?  // Recursion 🎉
}
3 Likes

We can write indirect enum, not only indirect case. @Indirect does not work at the type level: it does not let us write @Indirect struct.

In theory it might be possible to introduce support for wrappers that work with both properties and every instance of a type (i.e. “type wrappers”), but that seems highly speculative. It would still not be able to support case unless we also allowed the use of property wrappers on cases (where they would wrap a tuple value). This direction seems unlikely to happen and I would like to see a single consistent and comprehensive solution for indirect in the language.

2 Likes

You’re welcome to pull anything I’ve written and include it. :slight_smile: