[swift-evolution] Remove Failable Initializers

Brent Royal-Gordon brent at architechies.com
Mon Mar 7 19:21:28 CST 2016


> A duplicate shouldn’t be necessary; functions/initialisers that can throw just need to be called with a flag indicating whether they should capture or ignore errors as appropriate for try vs try? and try!. Any statement that is identified as being specific to a throw is then wrapped in a conditional based on this flag so it can be skipped if errors are ignored.

Okay, so you're passing in what amounts to a new parameter (which is going to take up a register or some stack space that could be used for something else) and adding a new conditional branch at each throw site. In cases where a `try` is nested directly inside a `throws` function, you might need a conditional branch at the return site, too. (Remember, CPUs hate conditional branches.)

These things aren't free. In fact, they may end up costing more than the original optimization did.

> That’s more an issue of developer laziness more than anything

I think that your use of the word "laziness" is telling. You are assuming that, if someone doesn't throw detailed errors, they are being lazy. They are not putting in the work to write good code. They should be judged harshly for this. The language should punish them for their laziness by taking away the tools which allow it.

But sometimes when you don't do work, it's not because you're being lazy; it's because that work is *unnecessary*. You could spend time slaving over an enum full of error codes which will just be converted to an optional, or you could just return an optional in the first place and use the time you saved to do something that will actually improve the product in ways your users will value.

In many simple error handling situations, doing any work beyond indicating success/failure is *unnecessary*. It provides no additional value. We should not hinder the productivity of developers in situations like that simply because sometimes they might misuse the feature permitting it.

> but at least it’s still communicating that that’s exactly what the error is. If the standard library includes a good set of default errors then that should cover most use-cases; anything that isn’t covered by a standard error meanwhile absolutely should be given a new error type IMO.

But "invalid parameter" only communicates what's happening because it's so vague that almost any error can be called an "invalid parameter". A malformed path, a path to a file that doesn't exist, and a path to a file in the wrong format are all "invalid parameters". I mean, I guess it tells you that the problem isn't in a property or global or something, but that's not exactly a surprise, is it?

Simply throwing "invalid parameter" conveys no *actually useful* information beyond "that wasn't right", which is exactly what `?` does.

>> As an aside, I hope you realize that adding a human-readable string is *not helpful*. Without a machine-readable representation, the error cannot be expressed in domain-specific terms ("The ID number included an 'o'; did you mean '0'?") or even easily localized.
> 
> I’m not sure what you mean by this exactly; if you mean that an error should include extra data that can be extracted from the error instance itself then that depends on the error.

What I'm saying is that *actually* providing errors with enough detail to be useful is hard work. A string typed directly into your source code, addressed to the developer using your API, in one particular language, with the error's details converted to text and interpolated into it, will not usefully convey the problem. You need to express the problem *programmatically*, and that means carefully cataloguing the causes of errors and deciding for each one how much detail the developer needs.

To illustrate, I spent ten or fifteen minutes examining IntegerParsing.swift.gyb so I could understand the failure cases of `Int.init(_:radix)`. To fully model all of the errors which can cause it to return `nil`, and without including any redundant information you could get from the string itself, you would probably need this enum:

	enum IntFromStringError: ErrorType {
		case EmptyString
		case NoDigits
		case NegativeUnsigned
		case TooLarge (at: String.UTF16View.Index)
		case NonDigit (at: String.UTF16View.Index)
		case DigitBeyondRadix (at: String.UTF16View.Index)
	}

(Note that this does *not* include several errors for invalid radixes which are enforced by preconditions. Evidently, a too-small or too-large radix is considered to be a programmer error.)

A couple of those cases could probably be collapsed into others; it could be three or four instead of six. But think about all that has happened here:

• We've exposed an implementation detail: the algorithm uses the UTF-16 representation of the string. We could convert the index, but then we would be doing work—I suspect invoking an O(N) algorithm!—purely to provide error information which many users would not need. Similarly, if we ever needed to change the implementation, we would either need to make an incompatible change to the error enum or waste time on a conversion. And if we don't include an index at all, then we're not providing enough detail to do anything useful with those errors.

• We still have not modeled the possible errors with full fidelity, because `NegativeUnsigned` is only possible on a `UInt`. We really ought to have separate enums for `IntFromStringError` and `UIntFromStringError`.

• We cannot actually promise that these are the only possible errors this can throw. Currently, you can throw any ErrorType from any throwing function. And in the future, resilience will allow later versions of a library to add cases to enums.

• And we have provided the errors with a level of detail that nearly all callers do not care about. 99.9% of callers will use `try?` and throw away all of this information, so why are we going to so much effort to model it?

And again, if your answer is "Don't do all that, just lump everything together into one vague error", then why are we using the throwing mechanism in the first place? Failable initializers convey one vague error just as well and with much less fuss.

> If you’re capturing an invalid parameter error then it will be for debugging purposes I think, so the message is for the developer to inform them of which parameter failed (rather than having to have a different error for each parameter) and possibly some info on why, i.e- I’m not talking about text that would be spit out to a user or logged directly (except perhaps as a debug statement), or that you would expect to capture and process extensively. In this case even a simple string should be plenty helpful compared to nothing.

If an error should only occur during debugging, it should be a precondition, not a failable *or* throwing initializer.

By definition, any throwable error should be something you expect to happen in the normal course of running the code out in the wild. That means there needs to be enough detail to either automatically fix the problem or usefully present it to a user, either textually or with some kind of graphical representation (like pointing to the invalid character).

* * *

Ultimately, here is my point: When faced with a requirement that all errors be thrown, you get two choices.

1. **Throw something super-vague.** This is no better than returning `nil`.
2. **Throw something really specific and useful.** This requires engineering effort which might be better spent elsewhere.

Both options waste computer time and programmer time, and they ultimately stem from prejudging how the developer chooses to allocate their time.

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list