What is the best way to spell a setter that can fail?

Howdy!

I have a structure like the following

struct IPSuite {
  var ip: IPv4Address
  var mask: IPv4Address.SubnetMask
  ...
}

My problem is that not all of the potential states of IPSuite are valid in my use case, and I would like to provide an interface that prevents a user from entering such a state. e.g.

// This is fine
var suite = IPSuite(ip: IPv4Address("255.0.0.0"), mask: ._7, ...) ✅
// However this is not OK because the masked address becomes a network address 
suite.mask = ._8 ❌

I am aware that the above interface is not possible for several reasons (e.g. can't have a setter that throws, and it would require a try even if you could), but my question is what is the best (i.e. most swity) alternative for setters that can fail? I can think of alternatives (failable initializers, throwing initializers, factory methods, setter functions), but none of them seem especially great (especially for larger structures). How would you do it?

Thanks,
smkuehnhold

Could you make your setter private and only call it from a method that throws and validate your inputs in that function?

2 Likes

I could. But in reality, my IPSuite has several (10s of) properties that must be defined at init time, so it becomes very tedious.

My idea around this would be a factory method on an instance (makeCopyWith(ip: IPv4Address?, ...)), but I was wondering if there was anything better.

If you’re wanting to do this at init there’s failable initializers.

2 Likes

I understand, but what I am trying to do is avoid this dance (in the most conventional way)...

// suite is a previously defined IPSuite
guard let suite = IPSuite(
  ip: suite.ip,
  mask: newValue,
  param3: suite.param3,
  param4: suite.param4,
  param5: suite.param5,
  param6: suite.param6,
  ...
) else {
  return
}
...

Using failable initializes becomes painful when you have more than a few parameters.

Also I communicated poorly before. Ideally, I would want failure to occur at any time an IPSuite is put into a state that is invalid, not necessarily just init (though that may be the only define values in the eventual implementation of seems).

If the process can fail it needs to be handled somewhere. Do you have a vision of how you wish it would work? Perhaps we can help you get closer to that.

1 Like

I tried to communicate that in the initial post, but have clearly failed :disappointed_relieved:. I would say that unfortunately I don't exactly have a vision. My question was really one about what the convention is to what I suspect to be a common pattern. If what you suggest is that, then what you have given to me is the solution (though I can't say that I am not surprised).

Basically, I am unsure of the idiomatic way to define setters that can fail. It seems that exists with getters (throwing getters, optional getters), but not with setters.

Thanks for taking your time to respond to me.

1 Like

Sorry, I guess what I meant to ask is, how would you want the line marked with :x: to fail in your sample code? Return silently without modification of the mask property? Emit some kind of Error? Crash?

I’m not a Swift expert by any stretch, but hopefully if we can clearly spell out the intention one of the many experts here could tell us the “right way” to accomplish it. I think if I were doing this I’d have private vars and setter methods that throw, so you could try? suite.validateAndSetMask(._8) (or try and catch), but there indeed might be a better way.

It does make me want to go find the Evolution pitch for failable getters and see why setters don’t have the same option. Perhaps there was a technological hurdle that’s been since overcome and it’s something that could be pitched now.

1 Like

Looks like there was a pitch for throwing getters/setters but the actual implementation we got in Swift 5.5 for throwing getters came from a different proposal.

1 Like

That looks reasonable enough given a (seeming) lack of a better alternative. Thanks for the help :slightly_smiling_face:.

Would a property wrapper help?

1 Like

Do you mind clarifying what that would look like? :thinking:

Well depending on the behavior that you want, you can have property wrappers that can clamp values when you set them, etc. One way to do that is described here.

1 Like

Ah. Unless I am missing something, I am not sure if this is viable for my use case. I don't think there is method to automatically recover from being put in an invalid state. I think it would require manual intervention.

You’re going to have to have the validation somewhere in your business logic and decide how recovering from an invalid state happens, though, right? Can you do it in didSet for each property that requires validation?

1 Like

Unless I provide some kind of delegate, I can't see how this would work unfortunately. In my case, the IPSuite will most likely be used as a kind of view model. Meaning that I intend for the errors to propigate the end user and for them to provide the correction explicitly.

One of the alternatives that was briefly mentioned above is precondition. If you think your API should handle incorrect input the same way as Array treats index out of bound that's the one.

And if you are after throwing, it's trivial to change suite.mask = ... to try suite.setMask(...)

1 Like

It is not clear to me what you are trying to do, but if you want to prevent suite.mask from being assigned certain values, did you try to capture that in the didSet function?

Cheers,

1 Like

Without getting overly into functional programming minutia/techniques, a working technique is to use a builder pattern for changes. Swift key paths are alright for that kind of thing if you have a ton of properties and don’t want to use any codegen.

This falls down for globals, where you really do need mutation at times. That said, depending on how you are doing error handling for invalid states and where this input data is coming from (UI input vs setting files, etc), that may actually be a feature though. You can try to build a slightly modified object, and if a valid one results, set it to the global reference.

If you need to set several properties before running the validity check, confirm/commit/lock interfaces like you see with old UITableView animation and Camera apis aren’t a bad choice.

1 Like