Fun with range deprecations

H. S. Teoh via Digitalmars-d digitalmars-d at puremagic.com
Mon Aug 11 15:57:50 PDT 2014


So, a recent Phobos deprecation introduced a fun regression:

	https://issues.dlang.org/show_bug.cgi?id=13257

While the bug was filed specifically for the use case
range.splitter.map, the problem is actually much more general, and far
from obvious how to address.

First, let's consider the following setup.  Start with this example
range:

	struct MyRange(T) {
		// input range
		@property bool empty() { ... }
		@property T front() { ... }
		void popFront() { ... }

		// forward range
		@property MyRange save() { ... }

		// bidirectional range
		@property T back() { ... }
		void popBack() { ... }
	}

Suppose we write an algorithm that operates on ranges in general,
including MyRange. The current convention is to forward as many range
methods as possible, so that if you give it a bidi range, it will, where
possible, give you a wrapped bidirectional range. So we do this:

	auto myAlgo(R)(R range)
		if (isInputRange!R)
	{
		struct Result {
			// input range methods
			@property bool empty() { ... }
			@property U front() { ... }
			void popFront() { ... }

			// N. B. if we got a forward range, and we can
			// implement .save, then do so.
			static if (isForwardRange!R &&
				canDoForwardRange)
			{
				@property Result save() { ... }
			}

			// N. B. if we got a bidirectional range, and we
			// can implement .back and .popBack, then do so.
			static if (isBidirectionalRange!R &&
				canDoBidirectionalRange)
			{
				@property U back() { ... }
				void popBack() { ... }
			}
		}
		return Result(...);
	}

Now suppose, for whatever reason, we decide that implement MyRange as a
bidirectional range was a bad idea, and we decide to deprecate it:

	struct MyRange(T) {
		... // input & forward range API here

		deprecated("Use of MyRange as bidirectional range is now deprecated")
		{
			@property T back() { ... }
			void popBack() { ... }
		}
	}

Now the fun begins. Currently, isBidirectionalRange uses
is(typeof(...)) to check if .back and .popBack exist in target type.
So when myAlgo() checks MyRange, this check passes, because even though
.back and .popBack have been deprecated, they obviously still exist, so
they do have a valid type. So that check passes. Therefore, myAlgo()
will try to export a bidirectional interface in its result.

However, inside Result.back and Result.popBack, when it actually tries
to use MyRange.back and MyRange.popBack, the compiler throws up its
hands and say, Wait!! .back and .popBack are deprecated!!

Note that this happens *regardless* of whether .back and .popBack are
actually used by the user code that calls myAlgo(). This means that if
user code only ever uses forward range capabilities of myAlgo(), users
will *still* get irrelevant deprecation messages -- for bidirectional
range features that they never use! Furthermore, the only reason
myAlgo() added .back and .popBack which trigger the deprecation
messages, is because the is(typeof(...)) check tests positive for
bidirectional range support.

This problem, obviously, is not specific to bidirectional ranges, or,
for that matter, *any* range based code. It's a general problem with
wrapped types, of the form:

	struct Wrapped {
		deprecated void method();
	}

	struct Wrapper(T) {
		T t;
		static if (is(typeof(t.method())))
		{
			void func() {
				t.method();
			}
		}
	}

The problem here is that t.method() is generic, and it has specifically
tested for the usability of t.method(), yet when it tries to actually
use t.method(), it hits a deprecation message. How is it supposed to
know if t.method() has been deprecated? Currently we have no
__traits(deprecated) test. And even if we did, this may not be the best
solution (are we going to now insert __traits(deprecated) all over
generic code, on the off-chance that some random user type somewhere
will get deprecated in the unforeseeable future?).

So the question now is: how do we deal with this issue? Currently, this
problem already exists in Phobos 2.067a, and random user code that calls
splitter(...).map(...) will hit this problem.

So far the following have been suggested, none of which seem
particularly satisfactory:

1) Make Wrapper.func() a template function, so that the deprecation
message is not triggered unless the user actually calls it (in which
case the deprecation message *should* be triggered). The problem is that
when the deprecation message *is* triggered, it comes from deep inside
Phobos, and users may complain, why did you export a bidirectional range
API if that support is already deprecated?

2) Add a __traits(deprecated) or is(T == deprecated) test, and update
all affected sig constraints in Phobos to check for this. Doesn't sound
like an appealing solution.

3) Have std.algorithm.map specifically check for a specific overload of
std.algorithm.splitter, and omit .back and .popBack in that one case.
Very hackish, solves the immediate problem, but leaves the general
problem unsolved. User code that wraps around splitter in a similar way
to map will *still* be broken (even if they never actually use
bidirectional features).

4) Shorten the deprecation cycle of splitter. Doesn't even solve the
immediate problem, just shortens it, and still leaves the general
problem unsolved. User code that wraps around splitter is still broken
(even if they never actually use bidirectional features).

Since no obvious acceptable solution seems forthcoming, I thought we
should bring this to the forum for discussion to see if anyone has a
better idea.


T

-- 
If you're not part of the solution, you're part of the precipitate.


More information about the Digitalmars-d mailing list