Required Reading: "How Non-Member Functions Improve Encapsulation"

H. S. Teoh hsteoh at quickfur.ath.cx
Mon Oct 30 23:03:12 UTC 2017


On Mon, Oct 30, 2017 at 02:05:51PM -0600, Jonathan M Davis via Digitalmars-d wrote:
> On Monday, October 30, 2017 14:18:56 Steven Schveighoffer via Digitalmars-d 
> wrote:
> > On 10/30/17 1:40 PM, H. S. Teoh wrote:
> > > Page 2 of this article is essentially another reason why UFCS in D
> > > totally rawkz.  In D, we can take Scott's advice *without*
> > > suffering
> >
> > > from syntactic inconsistency between member and non-member
> > > functions:
> > You're missing a key piece here, in that anotherMethod does not
> > ensure the encapsulation of C. It can call 'method' just fine.
> >
> > Yes, it's great that UFCS can help with encapsulation via external
> > methods, but it's going to be difficult to prevent access to private
> > data, you have to use 2 modules. Not a very clean solution IMO.
> 
> Yeah, UFCS helps this whole concept, but the way that private works in
> D means that compiler-enforced encapsulation simply doesn't happen at
> the type level.
> 
> Another thing to think about is that private is private to the module
> rather than the class or struct partly on the theory that you should
> be able to keep track of everything in the module and maintain it
> appropriately so that things like whether the type is fully
> encapsulated aren't really an issue.

Yeah, the whole "private is module-private, not aggregate-private"
throws a monkey wrench into the works.  I can understand the logic
behind module-private vs. aggregate-private, but sometimes you really
*do* want aggregate-private, but D doesn't let you express that except
via splitting things up into submodules, which is a lot of overhead for
minor payback.

So my examples would have to involve submodules in order to really prove
the point.


[...]
> Still, UFCS does mean that it's less jarring to make something a free
> a function, and it then fits well with the cultural push we have to
> make functions generic, in which case, they're generally going to be
> free functions. But the gains from making something generic are clear,
> whereas the encapsulation gains of using a free function with UFCS are
> far less substantial, if they exist at all.
[...]

There's definitely gain here, IMO.  Part of the idea of encapsulation is
the independence of client code from implementation details, and one
such implementation detail is whether or not a function is implemented
as a class member or a free function. Ideally user code shouldn't need
to care about the difference.  In C++, however, user code has no choice,
because you can't write obj.func() when func is not a member.

But in D, UFCS allows obj.func() to work for both member functions and
free functions, so if the client code uses the obj.func() syntax, it
won't have to care about the difference.

One may argue about whether allowing arbitrary extensions to a class /
aggregate via UFCS is necessarily a good thing; but IMO, there are
definite benefits to it.  One classic example is std.range using UFCS to
essentially extend built-in arrays to have a range API, by providing
free functions .empty, .front, .popFront that take array arguments.  Of
course, whether this is a good thing can be argued for or against, but I
see this as just one example of a wider pattern of *adaptability*, which
IMO is a very good thing.

For example, suppose you're using a proprietary library that provides a
class X that behaves pretty closely to a range, but doesn't quite have a
range API.  (Or any other API, really.)  Well, that's not a problem, you
just write free functions that forward to class X's methods to bridge
the API gap, and off you go.  You don't have to work with your upstream
provider, who may not be able to provide a fix until months later, and
you don't have to create all sorts of wrapper types just to adapt one
API to another.

And if done right, if a new library version is released which breaks an
old API, say a method xyz() is deprecated and removed, or renamed, or
whatever, all you have to do is to write a free function named xyz, that
forwards to the new method(s), and you don't have to do a massive
upgrade of your entire codebase.  Another win for encapsulation.

(And yes, ideally the upstream library wouldn't break backward
compatibility, but we all know that in the real world it does happen
every now and then. And when it does, the last thing I want to be
worrying about is renaming 1500+ function calls to use xyz(p,q,r) syntax
instead of p.xyz(q,r) syntax. In C++, I'd have no choice, but in D, UFCS
lets my code become agnostic to this distinction, which is a very good
thing IMO.)

And as Scott already mentions, you can spice up the API you were given
with more convenience functions that do frequently-performed method call
sequences.  Carried one step further, this means if you have two
classes, presumably coming from two different vendors, with similar but
not-quite-the-same APIs, UFCS allows me to extend the two APIs so that
they converge.  Then my code can freely use objects from either library
without needing to care about API differences between them. Now,
*that's* encapsulation!

Basically, the more my code can become independent of API changes, the
better.  According to Scott's definition, that's an increase in
encapsulation, because the number of changes required to update my
codebase in the face of an API change is greatly reduced.  UFCS gives me
some pretty powerful tools in this regard.


T

-- 
Тише едешь, дальше будешь.


More information about the Digitalmars-d mailing list