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