Is `alias this` a mistake?

H. S. Teoh hsteoh at quickfur.ath.cx
Wed Aug 4 16:45:27 UTC 2021


On Wed, Aug 04, 2021 at 03:13:07PM +0000, jmh530 via Digitalmars-d wrote:
> On Wednesday, 4 August 2021 at 14:32:17 UTC, bachmeier wrote:
[...]
> > It's difficult to beat the simplicity of alias this. Even if
> > something cam be done with mixin templates, that's a lot of overhead
> > vs `alias x this;`. I can't say I use mixin templates very often,
> > but am skeptical that there's no way to misuse them.
> 
> I'm sympathetic to this.
> 
> I think if you got rid of alias this, then you would end up needing
> something else to accomplish some form of subtyping relationship for
> structs beyond just composition. For instance, the struct below can be
> used anywhere an int can be used. Without alias this, you either need
> to pull out that x value each time it is used in a function or you
> need to write overloads for all the functions you want to use Foo with
> like an int. Alternately, you can modify those functions to become
> generic and handle things that are like an int, but then what if you
> don't control those functions? It's just so simple.
> 
> ```d
> struct Foo {
>     int x;
>     alias x this;
> }
> ```

This is exactly why I used to love alias this. It lets me cowboy a
wrapper type into code that wasn't originally intended to handle it, and
it works wonderfully with minimal code changes.  I used to do this all
the time in my code, and it let me get away with quick fixes that for
the most part worked well.

Unfortunately, the long term effect of this sort of hackish solution is
a gradual degradation of maintainability and code readability. After a
while, when new code needs to be added, it's not clear whether I should
use type X, or wrapper type Y with alias this X, or wrapper type Z with
alias this Y (chained `alias this` used to be my favorite trick).  It
became context-dependent -- if I needed special functionality provided
by wrapper type Y, then I'd use Y; if Z provided something I needed at
the time then I'd use Z.  But soon I find out that I need *both* Z and Y
in the same function, so my code started to get cluttered with
interconversions between these wrappers.  Soon I had to invent yet
another wrapper type that encompasses *both* Z and Y just to get all the
data I need in one place. Which in turn leads to further such issues
later down the road.

Worse yet, (no) thanks to `alias this`'s super-convenient implicit
conversions, half of the code looks like it takes Y but actually
receives only X -- it's not immediately obvious because Y implicitly
converts to X. So when I need to propagate Y's functionality down into
code expecting only X, I find myself in a bind. And unlike actual OO
polymorphism there is no equivalent in `alias this` to an upcast: once Y
decays to X you cannot get Y back.  Which means that now, the code that
originally received only X had to be revised to receive Y.  Also, code
that receives X have no obvious connection to Y: there is no class
hierarchy that you can look up to learn possible relationships between
class X and class Y -- X can be the target of an implicit `alias this`
conversion of literally *any* type anywhere in the program. It's highly
unstructured subtyping that hurts long-term code readability and
maintainability.

Taking a step back from this cascading complexity, I realized that had I
*not* used alias this, I would have been forced to consider how to
encapsulate the functionality of X, Y *and* Z in a single common type
(or a proper class hierarchy) instead.  It would have been much less
convenient, but in the long run it would have improved code quality:
instead of the above spaghetti involving shuffling the same danged data
between X, Y, and Z, trying to figure out which type to accept and where
implicit conversions are happening, there would have been a single
unified type that everyone accepts.  There would be no question which
(wrapper) type to use because there would be no wrapper types.  And
there would be no spurious implicit conversions to trip you up when
reading the code.  The code would be cleaner and more consistent --
instead of various modules accepting different variations on X, Y, and
Z, there would be a single type that serves as common currency between
all code, eliminating a lot of needless complexity and implicit
conversion messiness.

This is why, even though I loved alias this (and still do to some
extent), I have come to realize that in the long term, it lies more on
the negative side of the scale than the positive.


T

-- 
It is not the employer who pays the wages. Employers only handle the money. It is the customer who pays the wages. -- Henry Ford


More information about the Digitalmars-d mailing list