[OT] What are D's values?

H. S. Teoh hsteoh at quickfur.ath.cx
Tue Oct 5 00:24:25 UTC 2021

On Mon, Oct 04, 2021 at 11:39:05PM +0000, Paul Backus via Digitalmars-d wrote:
> On Monday, 4 October 2021 at 22:15:33 UTC, Walter Bright wrote:
> > Interestingly, left off is one thing that D does very well at:
> > plasticity. What do I mean by that?
> > I discovered a key component of this is D's use of . instead of ->.
> > One can easily test drive with classes, structs, pointers, and
> > values, interchanging them as one would try on a shirt. It's such a
> > frackin' nuisance to do that in C and C++, one just doesn't bother.
> > 
> > This is plasticity, the opposite of brittleness.
> > 
> > What are your experiences with this?
> I've experienced this too.
> In D, with a little ingenuity, it is possible to make almost any
> surface-level syntax "desugar" to almost anything you want. Which
> means that when you want to change how some part of your program is
> implemented, you can almost always keep the original syntactic
> "interface" stable while doing so.

I've totally experienced this.  Some of my personal projects started out
as one-off, throwaway scripts, ugly, full of hacks, crazy one-liners,
etc..  But as time went on, it got refactored, cleaned up, sprouted
modules, packages, and gradually transformed itself into a set of
well-encapsulated components interacting with each other through nice
APIs, as though it were something designed that way from the beginning.

The use of '.' for what C/C++ use '.' and '->' for is one of the factors
in making this possible.  I'd even go further and say that the
coincidence of '.' (as in member access) with '.' (as in FQN separator)
allows certain refactorings that involve substituting a package/module
with a nested variable. (Actual example: to make I/O code more
unittestable, sometimes I insert an `alias stdio = std.stdio` template
parameter into my function; that way, the unittest can substitute
std.stdio with a mock-up struct that emulates std.stdio without
performing actual I/O. Had the FQN separator been different from the
member access operator, this would've been impossible.)

Optional parentheses is another - it's easy to turn a public mutable
field into a properly-encapsulated access method without needing to
rewrite a whole bunch of code.  Conversely, a method used like a field
can be substituted with an actual field, should that become necessary
for whatever reason.

Type inference and Voldemort types (much as people criticize it) makes
it possible to change the return value of some function without suddenly
making a whole ton of code uncompilable. Indeed, I've come to adopt the
practice of using `auto` whenever the code I'm writing technically
doesn't need to care what the concrete type is (which is quite a lot of
the time -- it applies even if you access some field in the type,
because all that requires is that it be any type for which .xyz returns
a value; it can be a public field, an access method, a UFCS function on
an int, whatever).

UFCS + IFTI also lets you abstract away the implementation details of
entire chains of processing, so you can change any number of
intermediate types in between without needing to touch the chain itself
at all.

> I think this "plasticity" is probably closely related to
> "expressiveness", because the language features that contribute to one
> tend to also contribute to the other.

IMO, the plasticity comes from what I call "symmetry", by which I mean
the mathematical (not aesthetic) sense of symmetry: some object X
remains unchanged under some set of operations Y.  More specifically, in
D there's a lot of "syntactic symmetry": some given piece of syntax such
as a line of code (the "object") remains unchanged under many forms of
refactorings (the "operations").  Such swapping out the types involved
in that line of code, changing something from class to struct, etc..

The greater the syntactic symmetry of a piece of code under the set of
refactorings, the easier it is to refactor the program while making only
minimal changes to the surface level code.

For example, UFCS chains.  By constraining the API of each component of
the chain to the range API, the chain becomes symmetric under many
refactoring operations like inserting/removing a component, substituting
one component for another, changing the concrete element types of a
component, reordering components (to some extent -- if the relevant
types are compatible), etc..  This symmetry makes it easy to change the
code: the syntax of the chain remains unchanged under the operation of
changing a return type, for example, so you only need to change the
return type, you don't have to change the syntax of the chain.  The
range API serves as the invariant amid these refactoring operations, and
this gives range-based code a high degree of symmetry under said

DbI is a another powerful example: by using static if to discover the
properties of a template argument instead of making assumptions about
the existence of, e.g., some property .xyz, you make that template
symmetric under the operation of swapping that argument for another type
that may not have a .xyz property. The template adapts itself to the new
argument, instead of no longer compiling because the .xyz that was
present in the old type doesn't exist in the new.  The body of the
template remains unchanged in the face of the change in behaviour of the
incoming type.  Symmetry.

On the converse side, the lack of symmetry in certain aspects of the
language makes certain refactorings more difficult.

One example is the struct/class by-value/by-ref dichotomy.  While
certain refactorings let you get away with simply writing `struct` in
the place of `class`, if there's code that depends on the by-ref nature
of the class, then more effort will be required to maintain the
correctness of the code, e.g., inserting `ref` into function parameters
in order to emulate the by-ref behaviour of the class.

Another example is D's integer promotion rules. If for whatever reason
you wish to change `int` to `short`, for example, you'll quickly run
into trouble with arithmetic expressions that now require a cast in
order to be assignable back to a short.

I'm sure you can find many other examples of asymmetry in D, but IMO D
does have a high degree of symmetry in many of the right places, and
that makes D code highly refactorable and plastic.  In other languages,
there tends to be a lot of asymmetry under many refactoring operations,
which makes it difficult to perform certain kinds of refactorings. This
effectively de-incentivises said refactorings, making the language less
plastic and programs less refactorable.


Spaghetti code may be tangly, but lasagna code is just cheesy.

More information about the Digitalmars-d mailing list