Home > Software engineering >  Extends Set's insert in swift for custom logic
Extends Set's insert in swift for custom logic

Time:08-25

I need to have custom logic in a Set that defines when a Hashable can be insert or not.

First I tried to solve this with a observer

var Tenants: Set<Tenant> = [] {
    willSet {
        // to the business logic here
        // ...

But in an observer i can not return an error. So I tried to extend Set to overwrite the insert method.

extension Set where Element == Tenant {
    @inlinable mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element){

        // .... do my logic here ...

        return (true, newMember)
    }
}

That works so far and the method will be called. I can return true and if my logic did not pass even a false. Ok, but how do I add the Element into the Set? super.insert(). The return is correct, but the Set is empty. How to add the elements into the concrete set?

Implementation so far

/// Global set of known tenants
var Tenants: Set<Tenant> = [] {
    willSet {
        let newTenants = newValue.symmetricDifference(Tenants)
        guard let newTenant = newTenants.first else {
            Logging.main.error("Can not find tenant to add.")
            return
        }
        Logging.main.info("Will add new Tenant \(newTenant.name) [\(newTenant.ident)]")
    }
}

extension Set where Element == Tenant {

    @inlinable mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element){
        print("Check to add...")
        // .... do my logic here ...

        // ok
        return (true, newMember)
    }

}

The result is:

Check to add...
error : Can not find tenant to add.
Check to add...
error : Can not find tenant to add.

CodePudding user response:

This seems to work for "do my logic here"

    self = self.union([newMember])

Edit: Because this breaks the semantics of Set, I think it is better to write it as something like this:

struct CheckedSet<T: Hashable> {
    private(set) var wrappedSet: Set<T> = []

    var shouldInsert: (T) -> Bool = { _ in true }

    mutating func maybeInsert(_ t: T) {
        guard shouldInsert(t) else { return }
        wrappedSet.insert(t)
    }
}

var cs = CheckedSet<String>()
cs.shouldInsert = { str in str.allSatisfy(\.isLowercase) }

cs.maybeInsert("HELLO")
cs.wrappedSet  // []

cs.maybeInsert("hello")
cs.wrappedSet // ["hello"]

CodePudding user response:

I would do it with a property wrapper:

@propertyWrapper
struct TenantsSet {
    var wrappedSet: Set<Tenant>
    
    struct Projected {
        let error: Bool
    }
    
    var projectedValue = Projected(error: false)
    
    var wrappedValue: Set<Tenant> {
        get { wrappedSet }
        set {
            print("some custom logic")
            // set projectedValue appropriately
            wrappedSet = newValue
        }
    }
    
    init(wrappedValue: Set<Tenant>) {
        wrappedSet = wrappedValue
    }
}

This allows error-reporting by checking the error property on the projected value:

@TenantsSet var tenants = []

func f() {
    tenants = [Tenant()]
    if $tenants.error {
        
    }
}

As the Swift Guide says:

Extensions add new functionality to an existing class, structure, enumeration, or protocol type.

You are not supposed to use them to modify existing behaviour. It would be very confusing to readers of your code. If you want to use an extension to do this, you should declare a new method, with a different signature. Perhaps call it insert(newTenant:)?

  • Related