A Philosophy of Software Design

H. S. Teoh hsteoh at qfbox.info
Mon May 25 00:47:07 UTC 2026


On Sun, May 24, 2026 at 02:00:35PM -0700, Walter Bright via Digitalmars-d wrote:
> On 5/24/2026 7:51 AM, H. S. Teoh wrote:
> > On the contrary, you yourself said back in the day that exception
> > handling was a good idea because it forced the program to stop upon
> > encountering an I/O error.  As opposed to a C program ignoring the
> > return value of printf(), et al, and blindly barging forward when an
> > unexpected condition like a disk full error started happening.
> 
> Yes, I did say that. But as I also wrote, my thinking has evolved over
> time and experience.
> 
> printf does two things:
> 
> 1. format arguments into a character stream
> 
> 2. write the stream to stdout.
> 
> #2 is where the errors happen. #1 is without error (at least in D!).

IMO, the problems stem from conflating these two distinct operations
into one.  Formatting a character stream doesn't have anything to do
with writing to stdout.  The former involves manipulating string data
in-memory; the latter is involves interacting with the OS and doing I/O,
which is always prone to errors and unexpected conditions.

The correct approach is to separate the two operations, which is
essentially what you propose:

> So, I have advocated the "sink" or "output range" approach, which
> would look like this:
> 
> ```
> printf(sink, format, args...);
> ```
> 
> printf shouldn't know anything about sink, it should just send the
> characters to it.

This is equivalent to separating concerns: printf is concerned with
formatting and only formatting; writing the output is handled by the
sink.  This immediately cleans up the horrible mess in C of endless
printf variants: fprintf, snprintf, vprintf, vfprintf, vsnprintf, ad
nauseum.

In fact, in D, we do have a separate .format() (.formattedWrite
underneath) and .write.  For convenience we have writef combine the two,
but it's really no more than syntactic sugar.  You could have just
written:

	formattedWrite(stdout.lockingTextWriter, fmt, args...);

The names could use some abbreviating, but the principle is sound.


> You can see this in the dmd implementation, where OutBuffer is the
> sink for formatted characters, and ErrorSink is used to accept
> messages about errors. By consolidating output into sinks, one does
> not have to check for an error at every invocation of printf.

But that just returns you to the original problem: if the sink carries
error information, someone needs to check it.  If nobody does, then the
code just barges along thinking everything is fine when the sink is
already in an error state.  No different from C programs ignoring the
return code of printf and doing silly things when e.g. the disk is full.

And as we all know, programmers are lazy; if error-checking is optional,
nobody will do it.

And enforcement doesn't work either: if you force people to handle error
conditions, they will just do the bare minimum they can to get away with
it.  Hence the proliferation of C functions that return -1 (or
equivalent dummy value) for any and every error condition, with no
indication of what actually went wrong.  Which leads to an entire
application getting into an error state from some obscure combination of
conditions, and the only message you can get out of it is "Internal
error".


> I still use printf in dmd, but only for the porpoise of printing
> debugging information so I can debug the compiler. I use snprintf()
> for when things matter, because snprintf() accepts a (primitive) sink
> as an argument.

(I'd love to be introduced to the porpoise that can help me debug
compilers. But I digress. ;-)

On a more serious note, I find D's .writefln a much better debugging
tool than C's printf.  Thanks to DbI, dumping the state of a complex
type is often as simple as `writefln("%s", myBigStruct)`, as opposed to
printf, where you have to manually spell out %-specifiers for every
field of the struct you wish to examine.  Combining writefln with UFCS
chains makes it even more powerful: I often use it to debug code that
handle multi-dimensional data; e.g., to dump 2D data stored in a 1D
array, I can just write:

```
writefln("%-(%-(%s, %)\n%)", myFlatArray.chunks(myWidth));
```

and instantly get nice output like this:

```
1 2 3
4 5 6
7 8 9
```

Try doing that in C, and you'll quickly find yourself spending an entire
afternoon writing a debug module just to format debug info in a
human-digestible way.  In D, just a few seconds to write out the nested
%-specifiers and you're on your way.


T

-- 
Obviously, some things aren't very obvious.


More information about the Digitalmars-d mailing list