Synthesizing the `isEmpty` property?

quite often, i find myself writing types that look like:

public 
struct Divergence
{
    // important! do not add fields without also 
    // updating the `isEmpty` definition!
    var x:[X]
    var y:[Y]
    var metadata:Metadata?
    var a:A?
    var b:B?
    var c:C?
    
    init()
    {
        self.x = []
        self.y = []

        self.metadata = nil
        
        self.a = nil
        self.b = nil
        self.c = nil
    }

which have an isEmpty property defined like:

    var isEmpty:Bool
    {
        if  case nil = self.metadata, 
            case nil = self.a,
            case nil = self.b,
            case nil = self.c,
            self.x.isEmpty,
            self.y.isEmpty
        {
            return true
        }
        else
        {
            return false
        }
    }
}

this feels really error-prone, because the isEmpty flag is used to delete unused descriptors, and if i add a stored property to the struct without remembering to update the isEmpty definition, data loss can occur.

would this be a good candidate for a compiler-synthesized conformance?

I think we'd want a more general feature to synthesize iteration over a type's members, such that runtime mirror-like facilities need not be used. You'd then define isEmpty in terms of this iteration.

How you force all properties to have their own isEmpty property is an unsolved problem. I am not sure that synthesis is the answer, though, as it's not always obvious what emptiness means to a type.

Perhaps the standard library could define isEmpty for the types it vends, and then synthesis of aggregate types could work like synthesis of Equatable conformance.

5 Likes

i usually use this in conjunction with a protocol Voidable which requires it:

protocol Voidable 
{
    init()
    
    var isEmpty:Bool
    {
        get
    }
}

even though it looks like a “DefaultConstructible” protocol (which i am aware is a no-no around here), in my opinion, its existence is justified because the isEmpty makes it more than “DefaultConstructible”.

The OP would still have the problem that it would not help if someone adds a property that isn't Voidable.

Array and Optional could be (retroactively) conformed to Voidable.

Will this do?

public struct Divergence: Voidable {
    var x: [X] = []
    var y: [Y] = []
    var metadata: Metadata?
    var a: A?
    var b: B?
    var c: C?
}

protocol Voidable: Equatable {
    init()
    var isEmpty: Bool { get }
}

extension Voidable {
    var isEmpty: Bool { self == Self() }
}

I don't like "Voidable" name though.

this is the blocker, because an Array (or general Collection) can only be Equatable if its Element type is Equatable. but isEmpty does not have anything to do with the element type, it just indicates whether or not the collection is empty.

I see. Can you give those types a dummy equatable implementation like this?

protocol DummyEquatable {}

extension DummyEquatable {
    static func == (a: Self, b: Self) -> Bool {
        fatalError("isn't called really")
    }
}

extension X: Equatable, DummyEquatable {}

Provided there was no previous Equatable implementation for the type and that you only call it in relation to the "isEmpty" check it will not be actually called, just make compiler happy.

Baking "isEmpty" autosynthesizing into the language itself would be tricky; the very first obstacle to overcome - it's rarely needed to worth the trouble.