Thread overview
isInputRange not satisfied even if all individual conditions are satisfied
Jun 26, 2020
Johannes Loher
Jun 26, 2020
ag0aep6g
Jun 26, 2020
Johannes Loher
Jun 26, 2020
Paul Backus
Jun 26, 2020
ag0aep6g
June 26, 2020
Recently, I read about problems regarding the API design of ranges (not being able to make them const, differences between class and struct based ranges etc.).

One of the issues that came up what the fact that sometimes it is useful to use the class wrappers `InputRangeObject` etc. if you need to decide at runtime which range you want to use. I ran into this into a personal project in the past and this also was how I solved the issue.

Another suggestion in that thread was to simply use choose, which I guess should work, but I had trouble getting it to work in my particular case.

However, a third option came to my mind: Why not simply use a sumtype to return either one range or the other? In order to make it more convenient to use afterwards, it would be useful if we could regard a sumtype of ranges as a range itself. That should be no problem because we just need to delegate all function calls to the range api to the actual range that sits inside the sumtype.

So I tried to implement this and it kind of seems to work (I can call the individual functions on the sumtype using UFCS) but for some reason, `isInputRange` evaluates to `false`. What makes it even weirder is the fact that all individual conditions that are part of `isInputRange` seem to be fulfilled. Could somebody explain to me what is going on? Here is the code:

https://run.dlang.io/gist/93bcc196f73b0a7c7aa1851beef59c22 (currently
give me a 502 though)

```
#!/usr/bin/env dub
/+ dub.sdl:
	name "sumtype-range"
    dependency "sumtype" version="~>0.9.4"
+/

module sumtype_range;

import std.meta : allSatisfy, staticMap;
import std.traits : allSameType;
import std.range : isInputRange, ElementType, empty;

import sumtype;

private template EmptyLambda(T) if (isInputRange!T)
{
    alias EmptyLambda = (ref T t) => t.empty;
}

@property bool empty(TypeArgs...)(auto ref scope SumType!(TypeArgs) r)
        if (allSatisfy!(isInputRange, TypeArgs) && TypeArgs.length > 0)
{
    return r.match!(staticMap!(EmptyLambda, TypeArgs));
}

private template FrontLambda(T) if (isInputRange!T)
{
    alias FrontLambda = (ref T t) => t.front;
}

@property ElementType!(TypeArgs[0]) front(TypeArgs...)(auto ref scope
SumType!(TypeArgs) r)
        if (allSatisfy!(isInputRange, TypeArgs)
            && allSameType!(staticMap!(ElementType, TypeArgs)) &&
TypeArgs.length > 0)
{
    return r.match!(staticMap!(FrontLambda, TypeArgs));
}

private template PopFrontLambda(T) if (isInputRange!T)
{
    alias PopFrontLambda = (ref T t) => t.popFront();
}

void popFront(TypeArgs...)(auto ref scope SumType!(TypeArgs) r)
        if (allSatisfy!(isInputRange, TypeArgs) && TypeArgs.length > 0)
{
    return r.match!(staticMap!(PopFrontLambda, TypeArgs));
}

enum bool myIsInputRange(R) =
    is(typeof(R.init) == R)
    && is(ReturnType!((R r) => r.empty) == bool)
    && is(typeof((return ref R r) => r.front))
    && !is(ReturnType!((R r) => r.front) == void)
    && is(typeof((R r) => r.popFront));

void main() {
	import std.range : iota, only;
    import std.stdio : writeln;
    import std.traits : ReturnType;

    auto i = iota(4);
    auto o = only(1);
    alias R = SumType!(typeof(i), typeof(o));

    // all individual conditions of `isInputRange` are satisfied
    static assert(is(typeof(R.init) == R));
    static assert(is(ReturnType!((R r) => r.empty) == bool));
    static assert(is(typeof((return ref R r) => r.front)));
    static assert(!is(ReturnType!((R r) => r.front) == void));
    static assert(is(typeof((R r) => r.popFront)));

    // but `isInputRange` is not satisfied
    static assert(!isInputRange!(R));

    // and neither is a local copy
    static assert(!myIsInputRange!(R));
}
```
June 26, 2020
On 26.06.20 15:09, Johannes Loher wrote:
> import std.meta : allSatisfy, staticMap;
> import std.traits : allSameType;
> import std.range : isInputRange, ElementType, empty;
[...]
> @property bool empty(TypeArgs...)(auto ref scope SumType!(TypeArgs) r)
>          if (allSatisfy!(isInputRange, TypeArgs) && TypeArgs.length > 0)
> {
>      return r.match!(staticMap!(EmptyLambda, TypeArgs));
> }
[...]
> enum bool myIsInputRange(R) =
>      is(typeof(R.init) == R)
>      && is(ReturnType!((R r) => r.empty) == bool)
>      && is(typeof((return ref R r) => r.front))
>      && !is(ReturnType!((R r) => r.front) == void)
>      && is(typeof((R r) => r.popFront));
> 
> void main() {
[...]
>      import std.traits : ReturnType;
[...]
>      alias R = SumType!(typeof(i), typeof(o));
> 
>      // all individual conditions of `isInputRange` are satisfied
>      static assert(is(typeof(R.init) == R));
>      static assert(is(ReturnType!((R r) => r.empty) == bool));
[...]
>      // but `isInputRange` is not satisfied
>      static assert(!isInputRange!(R));
> 
>      // and neither is a local copy
>      static assert(!myIsInputRange!(R));
> }

`isInputRange!R` fails because it has no knowledge of your free `empty` function. Without `empty`, `R` is obviously not a range.

`myIsInputRange!R` fails because you forgot to import `ReturnType` in module scope. You're importing it locally in `main`, so the check passes there.
June 26, 2020
Am 26.06.20 um 15:35 schrieb ag0aep6g:
> `isInputRange!R` fails because it has no knowledge of your free `empty` function. Without `empty`, `R` is obviously not a range.

Ah, OK, that makes sense. It's kind of sad though because it really limits the extensibility of existing types with UFCS. Do you know if there is a way around this?
June 26, 2020
On 26.06.20 15:35, ag0aep6g wrote:
> `isInputRange!R` fails because it has no knowledge of your free `empty` function. Without `empty`, `R` is obviously not a range.

To be clear: It's the same with `front` and `popFront`. You can't implement any range primitives as free functions.

It only works for arrays because those implementations are part of std.range.
June 26, 2020
On Friday, 26 June 2020 at 13:53:46 UTC, Johannes Loher wrote:
> Am 26.06.20 um 15:35 schrieb ag0aep6g:
>> `isInputRange!R` fails because it has no knowledge of your free `empty` function. Without `empty`, `R` is obviously not a range.
>
> Ah, OK, that makes sense. It's kind of sad though because it really limits the extensibility of existing types with UFCS. Do you know if there is a way around this?

You can use my recently-released Dub package `addle` to make your UFCS extensions visible to std.range.

Dub: https://code.dlang.org/packages/addle
Announcement: https://forum.dlang.org/thread/yruztugitygumwcsmjkc@forum.dlang.org