Conversation
camio
left a comment
There was a problem hiding this comment.
Hopefully this submits the comments I made here. They ma have gotten lost?
|
Looks like you made a different PR? Here are my comments on the old one: #39 (review) |
Thanks; they were good. I mistakenly deleted the branch in the server, which closed the PR. |
|
I was wondering if it is worthy to provide wisdom on some of common error handling (mis)conceptions widely spread in industry:
I have noticed the above patterns are common for "backend" applications (applications running on server, interacting with DB, etc). I know the points I mentioned has nothing to do with correctness of the program and might be very contextual but the reason for pointing this out is I have noticed multiple people exchanging multiple theories/libraries around similar ideas(an example from r/rust) without even talking about design by contract. Maybe addressing this would help them. |
| subscript itself, so it's much more efficient to pay that cost once in | ||
| the `subscript` implementation. | ||
|
|
||
| ### How to Choose? |
There was a problem hiding this comment.
A factor can be whether the handling of weak postcondition or the upholding of a stronger precondition is visible/noticeable in the caller or not. When adding a precondition that we only indicate in the documentation, it is easy to misuse the API. It's best if our preconditions can be encoded in the parameter types (an example can be passing a URL type rather than a String. This can save us from re-checking the preconditions in multiple places, thus save us some performance. Once the URL object has been created (parsed), its invariants are established, so all subsequent use sites can assume it's valid without a doubt.
Weakening the postcondition by returning an Error<T>/Optional<T> or throwing an exception have the advantage that they make the caller aware that there may be a possible failure, which they must handle/forward. This is useful for functions whose preconditions we cannot guarantee to satisfy, e.g. fileManager.deleteFile(existingFilePath:).
I'm generally in favor of adding strong preconditions, but I prefer to encode as much of them in types as possible, because too many (checked or unchecked) preconditions make an API hard to use.
There are also cases when the callers likely don't have local reasoning about whether the preconditions are already ensured or not. An example for this is an asset manager, where each asset can be in a loaded or unloaded state, and methods load(:), reload(:), unload(:) are exposed, with their trivial applicable preconditions. When a caller wants to load an asset, they likely won't know if the asset has been loaded by something else, and thus would need to do something like this:
func ensure_asset_loaded(asset: inout Asset) {
if !asset.loaded {
asset.load()
}
}
func ensure_asset_reloaded(asset: inout Asset) {
if asset.loaded {
asset.reload()
} else {
asset.load()
}
}
func ensure_asset_unloaded(asset: inout Asset) {
if asset.loaded {
asset.unload()
}
}I had a similar case at work, where I kept the load/reload/unload functions private while exposing the rather goal-oriented and easier to use alternatives publicly, and it helped getting rid of a class of bugs that were happening due to the previous code trying to reason about those conditions with non-local reasoning.
There was a problem hiding this comment.
This seems to be a long way of saying, when I mention strengthening preconditions, I should make a reference back to the previous chapter when I said the most effective way is to encode them in the type system. Am I missing anything? That mention should go earlier in the text, where I first discuss it as an option.
There was a problem hiding this comment.
Actually now that I look at the text, I think it is more complex than that, and I don't see a very clear reference to it in the contracts chapter. Thanks for the suggestion!
| A useful middle ground is to describe reported errors at the module | ||
| level, e.g. | ||
|
|
||
| > Any `ThisModule` function that `throws` may report a | ||
| > `ThisModule.Error`. | ||
|
|
||
| A description like the one above does not preclude reporting other | ||
| errors, such as those thrown by a dependency like `Foundation`, but | ||
| calls attention to the error type introduced by `ThisModule`. |
There was a problem hiding this comment.
I like this middle ground. Whatever error doesn't conform to the mentioned error protocol can be usually still handled by the same handling method, it just wouldn't be as helpful. E.g. if something doesn't conform to the LocalizedStringConvertible protocol as suggested by the method's documentation, we can still display the non-localized description to the user. In a robotics system, any non-conforming error can be regarded as a fatal subsystem failure, triggering the emergency stop procedure at the top level.
I think there would be also value to have a language feature that can add a similar annotation for the module: "All throwing functions in module/scope/file X, unless otherwise specified, must throw something of a given type." Then we can express our intents more explicitly, e.g. with a required isFatalSubsystemFailure flag.
When writing a compiler, we generally want to throw Diagnostics, but and it's useful to know if the function can throw anything else, or if the function only throws the expected Diagnostics, but otherwise it's not doing any sketchy stuff. When we have that information, we can wrap throwing a function into something that returns a partial AST and a set of diagnostics, without the need to rethrow any extra errors.
There was a problem hiding this comment.
I don't understand what you're getting at with isFatalSubsystemFailure, but fatal errors are distinct from the recoverable kind we report by throwing. You don't want to unwind the stack if the condition is going to be fatal.
Also, is there a suggestion for the text here or are we just chatting?
There was a problem hiding this comment.
Fatal means slightly different things in a robotics context than in a regular application. When an application experiences a bug, it is reasonable to trap and expect the user to restart the application if they want. However, in a robotics system, we may need to perform safety measures, such as lowering a motorized arm slowly to avoid falling and damaging components. Often, the emergency stop also doesn't just cut the power but e.g. keeps holding onto suction cups so the robot doesn't drop a 10kg glass window.
Also, fatal subsystem failures can likely happen due to broken sensors, failed communication with motors, and I think should be distinguished from our regular fatal failures caused by bugs/precondition violations. Subsystem failures can lead to the graceful termination of that specific subsystem, while the same controller may go on with finishing some remaining tasks of other subsystems.
(I don't have any suggestions to the text, just discussing.)
|
|
||
| ```swift | ||
| extension Array { | ||
| /// Exchanges the first and last elements. |
There was a problem hiding this comment.
If we change the condition count() == 1 to count() <= 1, we can easily make this function safe even if the precondition is turned off. I think this is orthogonal here, but still slightly distracting/weakening the argument.
There was a problem hiding this comment.
Maybe it would be worth mentioning this example still though. What's the tradeoff between writing the two different conditions, and what is the ideal precondition of this function?
There was a problem hiding this comment.
That is really not the point of the example, so I don't want to get into that here. However, I would welcome a better example that doesn't raise the concern (which occurred to me too).
| In most cases, the only acceptable behavior at that point is to | ||
| present an error report to the user and leave their data unchanged, | ||
| i.e. the program must provide the strong guarantee. That in turn | ||
| means—unless the data is all in a transactional database—a program |
There was a problem hiding this comment.
I wrote a transactional undo/redo framework that lets us compose commands similarly as functions, ensuring transactional guarantees on every level. It may be useful for more general programming tasks other than implementing undo/redo in editors, as we may often not want to discard the changes to the whole document, just the changes done in a specific scope.
See an example at: https://github.com/tothambrus11/undonete-swift/blob/master/Tests/UndoneteTests/UndoneteTests.swift
There was a problem hiding this comment.
Transactional guarantees don't compose, so using them at every level is incredibly inefficient. Is this a general remark or do you think the text should change somehow?
There was a problem hiding this comment.
No need to change the text, just discussing.
My composite command system achieves composable transactionality, assuming all low-level commands are correctly implemented to be transactional with an undo, a redo and initial execution method.
Higher level commands are composed of the execution of lower level commands (which may be themselves composite). Once any command fails to execute, it throws an exception, and the composite command undoes all previously executed subcommands, so that the composite command either fully succeeds or leaves the state in the original state.
There is a bit of syntactic overhead over regular programming, but I think explicit undoable commands can be generally useful when making a copy of the original state is unfeasible while requiring transactionality, and it makes code much easier to reason about, as it takes care of a semantically correct unwinding.
Co-authored-by: Ambrus Tóth <32463042+tothambrus11@users.noreply.github.com>
|
@RishabhRD I would need some suggestions of what specific wisdom to offer. I thought about all this and it seems mostly irrelevant to the core issues, so I didn't know what to say about it. If you want to wrap your errors with context information, go ahead; it can be useful… I guess the one advice I'd give is to use a higher order function, e.g. try withContext("What I'm doing right now") {
// the code
}but even that seems like I'd have to set up a lot of context in the text to even mention it. |
|
|
||
| Errors come in two flavors:[^common-definition] | ||
|
|
||
| > - **Programming Error**, or **bug**: code contains a mistake. For |
There was a problem hiding this comment.
| > - **Programming Error**, or **bug**: code contains a mistake. For | |
| > - **Programming error**, or **bug**: code contains a mistake. For |
| [Perhaps the earliest use | ||
| ](https://dl.acm.org/doi/10.1145/800028.808489) of the term “error | ||
| recovery” was in the domain of compilers, where the challenge, after |
There was a problem hiding this comment.
I have no reason to believe that the term error recovery was initially used in compiler design. See, for example, this 1959 paper, which talks about error recovery (or failure recovery) in the context of hardware issues. A quick search in Google Scholar found the term used as early as 1937.
| saw in the previous chapter, not all precondition violations are | ||
| detectable. Also, it's important to admit that when a precondition | ||
| check fails, we're not detecting the bug per-se: since bugs are flaws | ||
| in *code*, truly detecting bugs involves analyzing the program. |
There was a problem hiding this comment.
| in *code*, truly detecting bugs involves analyzing the program. | |
| in *code*, truly detecting bugs involves program analysis. |
| compromising security. If user data is quietly corrupted and | ||
| subsequently saved, the damage becomes permanent. | ||
|
|
||
| In any case, unless the program has no mutable state and no external |
There was a problem hiding this comment.
| In any case, unless the program has no mutable state and no external | |
| In any case, unless the program lacks mutable state and external |
| effects, the only principled response to bug detection is to terminate | ||
| the process. [^fault-tolerant] |
There was a problem hiding this comment.
| effects, the only principled response to bug detection is to terminate | |
| the process. [^fault-tolerant] | |
| effects, the only principled response to bug detection is process | |
| termination. [^fault-tolerant] |
|
|
||
| While, as we've seen, not all bugs are detectable, detecting as many | ||
| as possible at runtime is still a powerful way to improve code, by | ||
| finding detecting the presence of coding errors close to their source |
There was a problem hiding this comment.
| finding detecting the presence of coding errors close to their source | |
| detecting the presence of coding errors close to their source |
| While, as we've seen, not all bugs are detectable, detecting as many | ||
| as possible at runtime is still a powerful way to improve code, by | ||
| finding detecting the presence of coding errors close to their source | ||
| and creating an incentive to prioritize fixing them. |
There was a problem hiding this comment.
| and creating an incentive to prioritize fixing them. | |
| and incentivizing fixes. |
|
|
||
| Assertions are checked only in debug builds, compiling to nothing in | ||
| release builds, thereby encouraging liberal use of `assert` without | ||
| concern for slowing down release builds. |
There was a problem hiding this comment.
Aside: Swift got the defaults wrong here. 1) I don't think preconditions and inline assertions are fundamentally different such that one is in release and one isn't 2) For assert I think there should be two variations (assert and always_assert), but I prefer Rust's spelling: assert and debug_assert.
There was a problem hiding this comment.
Perhaps focusing on the desired properties in different contexts rather than taking Swift's naming and compilation scheme as given is more useful to the general audience. The precondition identifier serves also a documentation purpose, and it's a bit awkward to replace that to assert for saving performance in release builds.
It may be worth adding the explicit imaginary alternative precondition(condition, errorMessage, debugOnly=False), and disclose that different languages use different names and defaults with regards to tagging.
At the same time, it's useful to mention where to write assert and where to write precondition with regards to their semantic/documenting purpose.
| code in an unfinished state): | ||
|
|
||
| 1. Something your function uses has a precondition that you can't | ||
| be sure would be satisfied: |
There was a problem hiding this comment.
I'm having a lot of trouble understanding these examples. I'll try to think of something that's more straightforward.
better-code/src/chapter-3-errors.md
Outdated
| In general, when a condition *C* is necessary for fulfilling your | ||
| postcondition, there are three possible choices: | ||
|
|
||
| 1. You can make *C* a precondition of your function |
There was a problem hiding this comment.
I think it is worth clarifying (unless this is covered later) that option 1 is to add a precondition D such that when D holds, C holds.
It is sometimes desirable to add a precondition that is a strict superset of C, e.g. when it simplifies the function's interface.
sean-parent
left a comment
There was a problem hiding this comment.
Looking very good. My notes are relatively minor.
| > has a bug. | ||
|
|
||
| In the interest of progressive disclosure, we didn't look closely at | ||
| the idea, because behind that simple word lies a chapter's worth of |
There was a problem hiding this comment.
I would emphasis error in the quote, because even though you mention the
concept of errors above, it isn't clear if error is what "that simple word" is
referencing.
| recovery” was in the domain of compilers, where the challenge, after | ||
| detecting a flaw in the input, is to continue to process the rest of | ||
| the input meaningfully. Consider a simple syntax error: the simplest | ||
| possiblities are that the next or previous symbol is extra, missing, |
There was a problem hiding this comment.
| possiblities are that the next or previous symbol is extra, missing, | |
| possibilities are that the next or previous symbol is extra, missing, |
| compromising security. If user data is quietly corrupted and | ||
| subsequently saved, the damage becomes permanent. | ||
|
|
||
| In any case, unless the program has no mutable state and no external |
There was a problem hiding this comment.
| In any case, unless the program has no mutable state and no external | |
| Unless the program has no mutable state and no external |
| [^techniques]: Techniques for ensuring that restarting is seamless, | ||
| such as saving incremental backup files, are well-known, but outside | ||
| the scope of this book. |
There was a problem hiding this comment.
Since persistent transactions are a way to achieve both the strong guarantee and
to ensure restarting is seamless, they are probably worth a longer reference in
the text. Naming them as "persistent transactions" also gives the reader something
to research.
|
|
||
| While, as we've seen, not all bugs are detectable, detecting as many | ||
| as possible at runtime is still a powerful way to improve code, by | ||
| finding detecting the presence of coding errors close to their source |
| proper `x.randomShuffle()` would, and is not guaranteed to | ||
| preserve the same randomness properties. Perhaps more |
There was a problem hiding this comment.
We should state this more strongly than "not guaranteed." With quick or
introsort you get a high probability that the pivot element is near the center,
and the next pivot element is near either the 1/4 or 3/4 mark, and so on.
|
|
||
| - It makes it easy to identify incorrect code. A failure to satisfy | ||
| the condition becomes a bug in the caller, which aids in reasoning | ||
| about the source of misbehaviors. If the precondition is checkable |
There was a problem hiding this comment.
| about the source of misbehaviors. If the precondition is checkable | |
| about the source of misbehavior. If the precondition is checkable |
|
|
||
| ### When Propagation Stops | ||
|
|
||
| Code that stops upward propagation of an error and continues to run |
There was a problem hiding this comment.
| Code that stops upward propagation of an error and continues to run | |
| Code that stops propagation of an error and continues |
|
|
||
| Code that stops upward propagation of an error and continues to run | ||
| has one fundamental obligation: to discard any partially-mutated state | ||
| that can affect on the future behavior of your code (that excludes log |
There was a problem hiding this comment.
| that can affect on the future behavior of your code (that excludes log | |
| that affects the future behavior of your code (that excludes log |
| ``` | ||
|
|
||
| All mutations of a `DiskBackedArray` perform file I/O and thus can | ||
| throw. In the the `append` method, if if `ys.append(e.1)` throws, |
There was a problem hiding this comment.
| throw. In the the `append` method, if if `ys.append(e.1)` throws, | |
| throw. In the the `append` method, if `ys.append(e.1)` throws, |
instead of adding preconditions.
No description provided.