A Philosophy of Software Design
Richard (Rikki) Andrew Cattermole
richard at cattermole.co.nz
Sun Jun 28 05:25:38 UTC 2026
On 28/06/2026 2:35 PM, Walter Bright wrote:
> A reasonable question. Consider:
>
> ```d
> err = fprintf(stdout,"hello ");
> if (err)
> handleIt();
> err = fprintf(stdout,"betty\n");
> if (err)
> handleIt();
> ```
> as opposed to:
> ```d
> fprintf(stdout,"hello ");
> fprintf(stdout,"betty\n");
> if (stdout.isError)
> handleIt();
> ```
> Which would you prefer?
The first.
https://pubs.opengroup.org/onlinepubs/9699919799.orig/functions/fprintf.html
- EILSEQ: A wide-character code that does not correspond to a valid
character has been detected.
- EINVAL: There are insufficient arguments.
- For snprintf EOVERFLOW: The value of n is greater than {INT_MAX} or
the number of bytes needed to hold the output excluding the terminating
null is greater than {INT_MAX}.
Wait until you see what the underlying write call has to deal with:
https://pubs.opengroup.org/onlinepubs/9699919799.orig/functions/fputc.html#
This is what I deal with, and saying "go fix your kernel" is not a valid
answer. They are all like this, and you're lucky when most of the error
cases are even defined like this.
However neither is how I'd solve this if I could define the API.
```d
void myPrintff(FILE*, ref int error, ...) {
if (error) return;
...
}
int error;
myPrintff(stdout, error, "hello ");
myprintff(stdout, error, "betty\n");
if (error)
handleIt();
```
Or (this is the best out of the ways):
```d
struct BorrowedFile {
private bool haveError;
private bool checkedError;
File file;
void printf(...);
bool handleError() {
checkedError = true;
return haveError;
}
void printf(...) {
checkedError = false;
...
}
~this() {
if (!checkedError)
throw Exception("did not handle error");
}
}
BorrowedFile output = stdout.borrow();
output.printf("hello ");
output.printf("betty\n");
if (output.handleError) // missing static analysis to guarantee this
handleIt();
```
So an explanation, C standard IO is backed by pipes, they are inherently
non-transactional. Pipes may complete in an incomplete fashion or not at
all, they may fail to send for one call, but the next identical one succeed.
But even this isn't even remotely good for how BorrowedFile is being
called, because it ignores one key factor. One call can be cheap, but
the next could be expensive to get the arguments for, it may even have
side effects that you don't want if it failed. The only solution to this
is to... check the error. Although it could be more selective where.
```d
BorrowedFile output = stdout.borrow();
output.printf("hello ");
if (output.handleError)
return handleIt1();
output.printf(expensiveWithSideEffects());
if (output.handleError) // missing static analysis to guarantee this
handleIt2();
```
The by-ref error state variable can be used to great effect, I certainly
have. But you have to be quite careful with it. In above case its only
used for logging/output, that isn't too bad. But there are plenty of
other cases where you are NOT doing that! Its being used for actual
logic that feeds back in. And you can't ignore that the error occurred
as you can then get wrong results. Basically early returns can be
desirable instead via an exception.
For the static analysis capability its basically a combination of
@mustuse and result types force a check before a get. Where the
destructor is considered a get. Easy enough to implement.
Another solution that by-passes almost all of these problems, that
PhobosV3 is going in the direction of:
```d
string text = "betty";
int age = 2;
StringBuilder builder = new StringBuilder;
builder ~= i"hello $text you are ${age : 03d} old\n"; // throw on failure
consolePrint(builder.get);
```
And yes, that is real code that should compile once we are done. But
doesn't right now.
> I know I'm pushing upstream with this notion. I know it is not the
> conventional wisdom.
Unconventional? I don't think that matches my polite way to describe it.
Please read:
1. Mechanisms for compile-time enforcement of security -
https://dl.acm.org/doi/10.1145/567067.567093
2. Typestate: A programming language concept for enhancing software
reliability - https://ieeexplore.ieee.org/document/6312929
3. Typestate-oriented programming -
https://dl.acm.org/doi/10.1145/1639950.1640073
4. Typestates for Objects -
https://www.microsoft.com/en-us/research/publication/typestates-for-objects/
5. Linear Effects, Exceptions, and Resource Safety -
https://arxiv.org/html/2510.23517
6. Lightweight monadic regions -
https://dl.acm.org/doi/10.1145/1411286.1411288
Number 3, 5, and 6 talk about resources like files specifically.
> But it has a lot of potential to significantly
> simplify code and eliminate a lot of untested error paths. It is worth
> looking into.
That's actually your fault that they are untested in D. This isn't the
users fault ;)
With checked exceptions, where you know which expression, scope and
function can emit a given exception you can know if its a valid code path.
There is simply no way to model the code paths and its not fixable as
part of dmd's type system. The reason the fast dfa engine cannot analyze
catch statements is because of this.
More information about the Digitalmars-d
mailing list