Is it possible to automatically validate a value change and throw error?

In my current code I call validation method at the end of every CRUD method (if validation fails, an error is thrown and the change is reverted). I'm thinking if it's possible to remove these explicit validation calls. At first I thought it would be simple. Take the following code as an example, I just need to intercept the write access to value and do validation on the fly.

struct Name {
    var value: String
}

This thread lists the typical approaches to intercept access to internal value. I'd use computed variable (or property observer) for simple type and dynamic member lookup for complex type. However, automatic validation requires throwing error (otherwise how do we get notiifcation? Set a flag? But that requires explicit check too). Unfortunately none of these mechanisms supports throwing error. It seems it's impossible to implement it?

Since the above type is a basic type in the app (a basic type is used to build more complex type), you may think why not call validation method manually in these types, then we don't need do explicit validation for complex type. But that's not true. For example, Employee is a complex type built by using basic types. Let's suppose it has a rule that addr1 and addr2 shouldn't be same. So again we need to implement validation manually at the complex type level.

struct Employee {
    var name: Name
    var addr1: Address
    var addr2: Address
}

(PS: function body macro is another approach when it's available. But I'm still curious if it's possible to implement it with computed variable or dynamic lookup like features)

Two quick "manual" suggestions which might work for you:

  1. make the fields "let" instead of "var", incorporate the validation logic in the constructor and make it failable (either throwing or returning an optional)
struct Employee {
    let name: Name
    let addr1: Address
    let addr2: Address
    init(name: Name, addr2: Address, addr2: Address) throws { ... }
    init?(name: Name, addr2: Address, addr2: Address) { ... }
}
  1. do the validation on the outer level:
var employee: Employee {
    didSet {
        if !employee.isValid {
            self.employee = oldValue
        }
    }
}

the latter will temporarily invalidate the invariant, but as it is done "synchronously" it might not be a big deal, e.g. it will not cause a UI flicker through the incorrect value first.

Sorry that my incomplete example code mislead you. All types have CRUD methods. Your approach only works for immutable values.

My intuition is this approach doesn't work well in practice. For example, the code in didset has to be different if the outer struct has an array (or dictionary, or optional) of Employee. Not to mention it's error prone (easy to forget it) and hurts code readability a lot.

I'm going to use my current approach (doing validation in all CRUD methods that modify the value). Thanks.

Individual throwing setXxx methods?

Here's another approach based on dynamicMemberLookup (although you can have a throwing version of the subscript call):

@dynamicMemberLookup
struct Employee {
    struct Hidden {
        var name: String = ""
        var addr1: String = ""
        var addr2: String = ""
        var isValid: Bool {
            fatalError("TODO")
        }
    }
    private var hidden = Hidden()
    
    subscript(dynamicMember path: KeyPath<Employee.Hidden, String>) -> String {
        get {
            print("getter: \(path)")
            switch path {
            case \.name: return hidden.name
            case \.addr1: return hidden.addr1
            case \.addr2: return hidden.addr2
            default: fatalError()
            }
        }
        set {
            print("setter: \(path)")
            var newHidden = hidden
            switch path {
            case \.name: newHidden.name = newValue
            case \.addr1: newHidden.addr1 = newValue
            case \.addr2: newHidden.addr2 = newValue
            default: fatalError()
            }
            if newHidden.isValid {
                hidden = newHidden
            } else {
                // can't throw. print to console? fatalError?
            }
        }
    }
}

Can also be generalised to support different field types:

subscript<T>(dynamicMember path: KeyPath<Employee.Hidden, T>) -> T

usage:

var e = Employee()
e.name = "Hello"
print(e.name)

I don't get it. How are you going to throw the error? The hard part is not to call the validation method, but how to throw the error.

I don't why but subscripts are not throwing so indeed throwing would not be possible with dynamic member lookup based variant.

Do you use this at the moment?

struct Employee {
    private (set) var name: String
    private (set) var addr1: String
    private (set) var addr2: String
    
    func setName(_ name: String) throws {
        ...
    }
    func setAddr1(_ addr1: String) throws {
        ...
    }
    func setAddr2(_ addr1: String) throws {
        ...
    }
}

No, what I use is like the following (a regular approach):

public struct Employee {
    public var name: String
    public var addr1: String
    public var addr2: String
    
    public init(name: String, addr1: String, addr2: String) { ... }
   
    public func modify(name: String, addr1: String, addr2: String) { ... }

}

My question is not about ACL, but about if it's possible to make the code more concise (and robust) by removing explicit validation method call in init() and modify(). That said, I see your point and I should use private(set) (though that doesn't solve my original question).

And your "init" / "modify" are throwing?
You could share the validation logic between init and modify at least.
(and yeah, make the fields "private set" otherwise it would be too easy to bypass the validation).

Yes. I forgot it in the above code.

I agree. Thanks. My code are just used by myself, so I didn't pay much attention to ACL.

This simplify things irt boilerplate:

struct Employee {
    private (set) var name: String
    private (set) var addr1: String
    private (set) var addr2: String
    
    var valid: Bool { fatalError("TODO") }
    
    static func validating(name: String, addr1: String, addr2: String) throws -> Self {
        let result = self.init(name: name, addr1: addr1, addr2: addr2)
        if !result.valid { throw ... }
        return result
    }
    mutating func modify(name: String, addr1: String, addr2: String) throws {
        self = try Self.validating(name: name, addr1: addr1, addr2: addr2, validate: true)
    }
}

As the code is only for yourself - always remember using the "validating" static method to make "employees" and not the initialiser. As you can see the boilerplate amount is minimal. Alternative to a static creation method is an initialiser with a different signature (e.g. init(validating: true, ...)) but that's the same difference.

1 Like

That's a design to make sure it's impossible to bypass validation in the code. One nit: if you're doing it this way, the struct is immutable and I think all properties should be declared using "let". One reason I prefer to dynamic lookup (if it was feasible) is that it provides a explicit place in the code for handling value change, so I can do validation or send change notification in a single place. While the above design deals with validation perfectly, it doesn't provide direct support for handling value change (thought there is nothing to prevent me doing it in modify()). I'll think more about it and let you know if I use this approach in actual code. Thanks.

BTW, this can be guaranteed by declaring init() as private.


UPDATE: on a second thought, I think the above code can be improved by replacing the static method with a regular init() which has validation code? With that change, the design is essentially about implementing modify() by creating a new instance rather than performing in-place modification.

You could have the the individual fields writeable with something like this (more boilerplate):

struct Employee {
    var name: String
    { didSet { ifNotValidThen(revert: &name, to: oldValue) }}

    var addr1: String
    { didSet { ifNotValidThen(revert: &addr1, to: oldValue) }}
    ...
}

but you said you prefer them throwable, right? Then the explicit setXxx throwing methods.

yeah, I just wanted to not have the boilerplate of your own memberwise initialiser:

init(...) {
    self.name = name
    self.addr1 = addr1
    // and so on and so forth. boring!
    ...
    // at the end do the validation check
}

I don't know a way to make the built-in member wise initialiser private.

Personally, I don't think there's a big difference in writing these two statements:

try employee.modify(name: ..., addr1: ..., addr2: ...)
employee = try .init(name: ..., addr1: ..., addr2: ...)

We might be looking at a missing feature in the language, pseudocode:

struct Employee {
    var name: String
    var addr1: String
    var addr2: String
    
    didSet {
        print("value changed, old: \(oldValue)"
    }
    // similarly for willSet
}

It may be possible to do this with macros, just if we do have didSet on variables (and that's built in) we might well have it built-in on types.

Those setXxx methods are same as my modify() method in this sense.

I think it's feasible. For example, Entity macro add Validatable conformance; the struct implements the requirement of Validatable; and MemberInit calls validate(). I'm using this approach in my code for other purpose.

    protocol Validatable {
        func validate() throws -> Bool
    }
    
    @Entity
    @MemberInit
    struct Employee {
       let name: String
       let addr1: String
       let addr2: String
    
       func validate() throws -> Bool {
           ...
       }
    }

Isn't it just to replace public with private?

That's a cool feature indeed (although it still doesn't solve the "how to throw error" issue).

Can you show what you mean?
I fail to see how to have a built-in (automatically created) memberwise initialiser with a private on it.

I don't mean the one synthesized by compiler. I create init using macro, which is flexible.

// Default behavior: create a public init
@Init
struct Foo {
    ...
}

// Pass arguments to change its behavior
// The arguments are defined in macro interface module.
// Macro supports variadic arguments.
@Init(.private, .validating)
struct Bar {
   ...
}
1 Like