Anonymous Structs

I don't think that's an option - self.self is already defined in a different way.

Overall, I think the proposal doesn't pull its own weight, because it introduces a completely new syntax for a very narrow purpose.

Spelled like

it would be a different story, because this scheme has a much broader application - possibly too broad, as this syntax would not only be a natural choice for anonymous enums and classes, but also for functions.

As it seems to be extremely unlikely to replace the current syntax for closures with func (parameter: Type), the would not be a unified way to declare unnamed entities (or whatever is the best hypernym here ;-).

I agree with your assessment of 2–4.

I think the confusion induced by Option 1 (self is outer type) crossed with protocol extension methods (self is inner type) weighs heavily against that option.

As @Tino points out self.self is also already taken. So Option 0 is all that remains of these 6.

On reflection, that seems like a reasonable place to start. A future proposal could introduce shorthand outer-self if needed.

Hmm I think this is best explained by an example
Here we have two possible implementations of a heap that allows you to give a custom comparison function

Closure-based:

struct Heap<Element> {
	let compare: (Element, Element) -> Bool
	var elements: [Element]
	// Heap implementation
}

Struct-based:

protocol Comparator {
	associatedtype Element
	func isInOrder(first: Element, second: Element) -> Bool
}
struct NormalComparator<Element: Comparable>: Comparator {
	func isInOrder(first: Element, second: Element) -> Bool { return first < second }
}
struct InvertedComparator<Element: Comparable>: Comparator {
	func isInOrder(first: Element, second: Element) -> Bool { return first > second }
}
struct Heap<Compare: Comparator> {
	typealias Element = Compare.Element
	var compare: Compare
	var elements: [Element]
	// Heap implementation
}

In the closure-based heap, MemoryLayout<Heap<*>>.size is 24. No matter what the compare function is, it will always be stored as a pointer to the function plus a pointer to anything the function needs to run. If the compiler wants to inline it, it has to figure out where the heap was made to figure out what the function was.
In the struct-based heap, MemoryLayout<Heap<X>>.size is the size of X plus 8. It's the inline contents of the things the function needs to run, and the compiler knows the function from the type. If the compiler wants to inline it, it can since it can figure out what the function is from the type.

Now to use the example to answer your questions:

In a Heap<NormalComparator<Int>>, you can't just swap the compare function for an InvertedComparator<Int> halfway through. It's encoded into the type. If you wanted to be able to do that, then you would have to store a closure directly. If you want people to be able to choose between the two options, you have to be able to represent both. It's the same as the difference between AnyView and some View in SwiftUI. In AnyView, your view can change at any time. In some View, it's decided at compile time. As for "Why the new syntax", the original plan seems to have been to keep {} while some people want {{}}. Which syntax we use and whether we want anonymous structs are completely separate issues though.

In a function, it's probably the same (aside from the ability of structs to conform to protocols with possibly more than one method). But you can't inline into struct layout for structs that cross API boundaries. You can pass your Heap-containing-an-anonymous-struct to a Heap method that isn't @inlinable without switching its internal layout to have a closure instead of an anonymous struct, but even if your closure-based-heap could keep the closure contents in line through optimizations as long as you don't cross an API boundary, as soon as you do need to call a non-@inlinable function with your heap it has to go into the format required by the call, and that's with the closure contents not inlined. And if that function takes the heap inout, you can never inline it again, since you have no clue if the function swapped the closure or not.

1 Like

I agree. Option 0 is the one I that makes sense to me.

1 Like

How would you make Option 1 works without forcing the creation of a strong reference to parent self ?

This behaviour is a real issue in Java where inner classes (anonymous or not) implicitly keep a reference to parent self. This can be workaround with named inner classes by declaring them static, but can't be avoid with anonymous classes.

We don't want to paint us in a corner like Java does on this point.

1 Like

The options above only is only a matter of syntax. Outer self is intended to be captured - only if it used inside the anonymous structure/class, of course. Similar to closures you should be able to use capture lists to control how it is captured - strong/weak/unowned

I'm not an expert in Swift grammar, but looks like here struct is used in the expression context which should make it clear for the parser that this is a local struct definition. But I guess this might be confusing for people. Do you think adding a colon would be an improvement?

let eq = struct: Equatable { ... }
1 Like

Theoretically yes, but that‘s only a guess as I‘m no compiler dev.

Ok, looks like we have a decision. Let's move on to the question of the calling super initialiser for anonymous classes.

Few ideas from me:

Given these declarations:

let scrollView: UIScrollView = ...
let data: DataModel = ...
let lastOffset: CGPoint = ...

Option A

Explicit init method with no parameters

let delegate = class DelegateBase & UIScrollViewDelegate {
    // Shadows `lastOffset` from the parent scope, `lastOffset` from the parent scope is not captured
    var lastOffset: CGPoint 
    
    // Must be without parameters, because parameters are defined implicitly by capturing variables from the context
    init() { 
        // But we can still initialise extra properties
        lastOffset = .zero
        super(data: data)
    }

    override func reset() {
        scrollView.contentOffset = lastOffset
        /// Should we also allow `self.scrollView`?
        /// Probably not:
        /// * `scrollView` should be mentioned without `self.` at least once to be captured
        /// * Since stored properties are also accessible without `self.`, this creates a contracting logic:
        ///     - there is no property `scrollView`, so this name captures `scrollView` from the parent context
        ///     - but to capture it `scrollView` we need to make a property with name `scrollView`
        ///     - but then `scrollView` refers to the property
        ///     - so `scrollView` from the parent context is not captured
        ///     - so there there should be not property for `scrollView`   
        /// * not allowing leaves some room to change how captured variables are stored and named.
        self.scrollView.flashScrollIndicators()
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        ...
    }
}

Option B

Explicit init method even without parenthesis - extra work to do in parser when implementing

let delegate = class DelegateBase & UIScrollViewDelegate {
    var lastOffset: CGPoint
 
    init { 
        lastOffset = .zero
        super(data: data)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }
}

Option C

Call to super directly inside the class body

let delegate = class DelegateBase & UIScrollViewDelegate {
    super(data: data)
    var lastOffset: CGPoint = .zero
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }
}

Option D

Capture-list-like

let delegate = class DelegateBase & UIScrollViewDelegate { super(data: data) in
    var lastOffset: CGPoint = .zero
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }
}

Option E

Inside capture list

let delegate = class DelegateBase & UIScrollViewDelegate { [unowned scrollView, super(data: data)] in
    var lastOffset: CGPoint = .zero
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }
}

Option F

Java-like, after class name

let delegate = class DelegateBase(data: data) & UIScrollViewDelegate {
    var lastOffset: CGPoint = .zero
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }
}

Option G

Java-like, after class and all protocols

let delegate = class (DelegateBase & UIScrollViewDelegate)(data: data) {
    var lastOffset: CGPoint = .zero
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    }
}

My personal preference are A and D.

Wouldn't it be more consistent to move the anonymous struct decleration to the type position anyway?

like

let x : struct Equatable {
   let a: Int
   let b: Int
   init(a:Int,b:Int) { self.a=a;self.b=b }
} = .init(a: 5, b: 7)

How would it look like if anonymous struct is capturing something from the parent context?

F and G are not viable because the type information they rely on may sometimes be implicit in the type context rather than explicitly spelled out. Capture-list like syntax doesn’t feel right to me and neither does a call directly in the body so that rules out C-E. I can’t think of a compelling reason to deviate from the existing default initializer syntax so I think we should go with A.

Also, when super has a default initializer and all stored properties are initialized inline, we should allow the default initializer to be omitted and synthesize one that invokes super’s default initializer.

1 Like

Of these options, I prefer A.

More generally, I think the long declaration form should be as much like “nonymous” declarations as we can manage. That lowers the cognitive burden on developers. It also likely has the side benefit of making the implementation more straightforward.

To that end, I was wondering if it would be better to not use capture lists in the long form declaration at all. Instead, the developer could provide an init() whose explicit parameter names are names from the surrounding scope. The compiler would generate a call to this initializer, passing in the values from the surrounding scope. This lets developers perform calculations on those values without capturing them at all. And for values they wish to capture, they could assign to properties of the anonymous type. They would still have all the usual mechanisms to manage references (inout, weak, etc.)

Unfortunately, explicitly capturing inout references is verbose, requiring a dance with UnsafeMutablePointers and pointee.

Edit: here’s one way to do the capture dance:

struct Foo {
    var x: UnsafeMutablePointer<Int>
    init(_ x: inout Int) {
        self.x = UnsafeMutablePointer<Int>(&x)
    }
    
    mutating func boop() {
        x.pointee += 1
    }
}

var z = 0
var f = Foo(&z)
f.boop()
print("\(z)") // 1

This doesn’t work. Most of the time, the type context will provide type information and will not know whether it is going to receive an instance of a nominal type or an anonymous type. The syntax for anonymous types must be clear from the expression alone. Only the semantics will rely on a type context.

1 Like

That's an interesting case to consider, but I don't think it needs special syntax:

let x = 42
let c = class {
    var y: Int
    init() {
       // Desugared class will have a parameter in the initialiser for x, but will not capture it, because it is not used in other instance methods
       y = x * x
    }
}

Eep. I missed this implication.

Once we move past the single closure case, I think implicit capture might be a mistake. Capturing through multiple lexical scopes somehow feels surprising when those scopes have names (as methods would in the long form).

I agree that capture lists are not a good fit for the Java-like syntax. But I don’t think it should be necessary to declare an initializer that specifies a full signature for all captures. I think it should be possible to capture context in stored property declarations that include an initial value.

If we want to support something like this, I think the right way to do it is with some kind of “capturing initializer” syntax which would add a capture list to the initializer declaration somehow. If we do support this syntax, it should not be required - inline stored property initialization should still be supported.

I think implicit capture in stored property initialization and a default initializer makes sense. I don’t think implicit capture should be supported through the type body. All context required post-initialization should have to be explicitly captured in stored properties.

I think property declarations written for the sake of capturing are boilerplate code as much as initialiser arguments are. I would expect capture list to work through the type body. If you are concerned about being able to reason about the code, maybe it would more beneficial to have something like an explicit capture list?

For example, in C++:

int x = 1, y = 2;
// OK, autodetect and capture by value
auto a = [=]{ std::cout << x << y << std::endl; };
// OK, capture y by reference, autodetect and capture the rest by value
auto b = [=, &y]{ std::cout << x << y << std::endl; };
// capture only y by value
// error: variable 'x' cannot be implicitly captured in a lambda with no capture-default specified
auto c = [y]{ std::cout << x << y << std::endl; };

We could have explicit capture list as an opt-in for both closures and anonymous types:

let f = { only [y] in ... }

Without "constructor call", it appears to me that it's just a definition, not an object.
I've sometimes wondered if we could write it just like below, like a anonymous function.

var foo = struct Hashable {
    var x: Int
}(x: 2)