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

H. S. Teoh hsteoh at quickfur.ath.cx
Tue Oct 31 16:05:50 UTC 2017


On Tue, Oct 31, 2017 at 08:45:11AM +0000, Dukc via Digitalmars-d wrote:
> On Monday, 30 October 2017 at 23:03:12 UTC, H. S. Teoh wrote:
> > 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.
> 
> Yes, the idea here is great. It will work with range functions you
> define.  The problem is, it won't work with Phobos functions because
> they do not see your extensions to that proprietary type. They see
> only the true member functions of it.
> 
> Unless you hack phobos and add your extensions to that propietary
> library there, which would be a very ugly solution.
> 
> You can of course wrap that proprietary range type but that is a lot
> of manual work and requires maintenance. Alias this solves some cases
> but not all of them.
> 
> I am not sure what would be the best way for the language to handle
> this but for sure not the present way. The idiom is otherwise so
> great.

Haha, I knew somebody would bring this up.  I don't have a good solution
to this either.  The problem is that you can't export the type to Phobos
along with the additional UFCS stuff you tacked on to it.  I think
Phobos itself contains several hacks in order to work around this
problem for basic types, like importing std.array in generic code that
doesn't actually reference any arrays, but the import is necessary so
that arrays retain their range API.  Similar problems arise when you
pass user-defined types to Phobos where some methods are implemented as
UFCS.

The basic problem is that when a generic function in Phobos looks up a
method of type T, the lookup is done *in the scope of the Phobos
module*, not the caller's scope that passed in the T in the first place.
For example:

	/* usertype.d */
	module usertype;
	struct UserType {
		int memberMethod(Args...)(Args args);
	}

	/* ufcs.d */
	module ufcs;
	import usertype;
	int ufcsMethod(Args...)(UserType u, Args args);

	/* main.d */
	module main;
	import usertype, ufcs;
	void main() {
		UserType u;
		int x, y, z;

		// Lookup happens in module main, function main.  Since
		// usertype.UserType is visible here, .memberMethod
		// resolves to usertype.UserType.memberMethod.
		u.memberMethod(x, y, z);

		// Lookup happens in module main, function main.  Since
		// ufcs.ufcsMethod is visible here, .ufcsMethod resolves
		// to ufcs.memberMethod.
		u.ufcsMethod(x, y, z);

		// Calls Phobos function
		u.find(x);
	}

	/* snippet of std.algorithm */
	module std.algorithm;
	...
	HayStack find(HayStack, Needle)(HayStack h, Needle n) {
		...
		// Lookup happens in module std.algorithm. Since we
		// don't have the import of module ufcs here,
		// .ufcsMethod cannot be resolved.
		h.ufcsMethod(n);
		...
	}

I wonder if there's an easy way to extend the language so that you can
specify which scope a function lookup will happen in.  Suppose,
hypothetically, we can specify a lookup to happen in the caller's
context.  Then UFCS would work:

	/* snippet of std.algorithm */
	module std.algorithm;
	...
	HayStack find(HayStack, Needle)(HayStack h, Needle n) {
		...
		// Hypothetical syntax:
		with (HayStack.__callerContext)
			h.ufcsMethod(n);
		// Now lookup happens in the caller's context, i.e.,
		// module main, function main.  Since module ufcs is
		// visible there, this call resolves to ufcs.ufcsMethod.
		...
	}

The problem with this is that allowing the callee to access the context
of the caller opens up a can of worms w.r.t. symbol hijacking and
accessing local variables in the caller, which should be illegal.

Seems like the only workable solution in the current language is to wrap
the type in a custom type.  But like you said, that's high-maintenance,
and decreases encapsulation because when the implementation of UserType
changes, the wrapper type needs to change accordingly.  And the current
implementation of alias this is not without its own set of problems.

There *is* a generic wrapper type in Phobos that uses opDispatch for
member forwarding, but last time I checked, it also comes with its own
set of issues.  So currently, we still don't have a perfect solution for
this, even though we're so tantalizingly close.


T

-- 
Unix is my IDE. -- Justin Whear


More information about the Digitalmars-d mailing list