www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - wrong isInputRange design

reply rumbu <rumbu rumbu.ro> writes:
import std.range.primitives: isInputRange;

void test(R)(ref R range) if (isInputRange!R)
{
     auto c = r.front; //Error: no property 'front' for type 
'string'	
}

string s = "some string";
test(s);

The problem is that isInputRange will always return true for 
string types (because it will check the availability of front, 
popFront, empty through the entire std.range.primitives module.

Since the only thing that was imported is std.range.primitives: 
isInputRange, front is not available for strings.

OK, the simple resolution is to import entirely 
std.range.primitives, but the source of the problem was another: 
I tried to define two overloads, one accepting strings, and one 
accepting ranges;

import std.range.primitives: isInputRange, ElementType;
import std.traits: isSomeChar;

void foo(C)(const(C)[] array) if (isSomeChar!C)
{
     //this will be never called	
}

void foo(R)(ref R range) if (isInputRange!R && 
isSomeChar!(ElementType!R))
{
     //this will be always called
     //any call to range.front, range.empty and so on will result 
in error
}

foo(somestring)

I expect foo!string to be called instead of foo!Range, because in 
my context isInputRange!string should return false. Instead, my 
context is hijacked by the definitions spread along 
std.range.primitives module.

The workaround I found is to define the second overload like this:

void foo(R)(ref R range) if (isInputRange!R && !isSomeString!R && 
isSomeChar!(ElementType!R))
Dec 03 2016
parent reply Jerry <hurricane hereiam.com> writes:
On Saturday, 3 December 2016 at 11:52:00 UTC, rumbu wrote:
 import std.range.primitives: isInputRange;

 void test(R)(ref R range) if (isInputRange!R)
 {
     auto c = r.front; //Error: no property 'front' for type 
 'string'	
 }

 string s = "some string";
 test(s);

 The problem is that isInputRange will always return true for 
 string types (because it will check the availability of front, 
 popFront, empty through the entire std.range.primitives module.

 Since the only thing that was imported is std.range.primitives: 
 isInputRange, front is not available for strings.

 OK, the simple resolution is to import entirely 
 std.range.primitives, but the source of the problem was 
 another: I tried to define two overloads, one accepting 
 strings, and one accepting ranges;

 import std.range.primitives: isInputRange, ElementType;
 import std.traits: isSomeChar;

 void foo(C)(const(C)[] array) if (isSomeChar!C)
 {
     //this will be never called	
 }

 void foo(R)(ref R range) if (isInputRange!R && 
 isSomeChar!(ElementType!R))
 {
     //this will be always called
     //any call to range.front, range.empty and so on will 
 result in error
 }

 foo(somestring)

 I expect foo!string to be called instead of foo!Range, because 
 in my context isInputRange!string should return false. Instead, 
 my context is hijacked by the definitions spread along 
 std.range.primitives module.

 The workaround I found is to define the second overload like 
 this:

 void foo(R)(ref R range) if (isInputRange!R && !isSomeString!R 
 && isSomeChar!(ElementType!R))
Is that the exact code? isInputRange checks to see if the type has "front" defined. https://github.com/dlang/phobos/blob/v2.072.0/std/range/primitives.d#L162 Also "string" is just an alias of an array, "immutable(char)[]". So an array should have "front" defined. Can you post more code?
Dec 03 2016
next sibling parent Nick Treleaven <nick geany.org> writes:
On Saturday, 3 December 2016 at 16:37:21 UTC, Jerry wrote:
 Also "string" is just an alias of an array, 
 "immutable(char)[]". So an array should have "front" defined. 
 Can you post more code?
You still have to `import std.range.primitives : front`, even for arrays.
Dec 03 2016
prev sibling parent reply rumbu <rumbu rumbu.ro> writes:
On Saturday, 3 December 2016 at 16:37:21 UTC, Jerry wrote:
 On Saturday, 3 December 2016 at 11:52:00 UTC, rumbu wrote:
 import std.range.primitives: isInputRange;

 void test(R)(ref R range) if (isInputRange!R)
 {
     auto c = r.front; //Error: no property 'front' for type 
 'string'	
 }
[...]
 import std.range.primitives: isInputRange, ElementType;
 import std.traits: isSomeChar;

 void foo(C)(const(C)[] array) if (isSomeChar!C)
 {
     //this will be never called	
 }

 void foo(R)(ref R range) if (isInputRange!R && 
 isSomeChar!(ElementType!R))
 {
     //this will be always called
     //any call to range.front, range.empty and so on will 
 result in error
 }

 foo(somestring)

 I expect foo!string to be called instead of foo!Range, because 
 in my context isInputRange!string should return false. 
 Instead, my context is hijacked by the definitions spread 
 along std.range.primitives module.

 The workaround I found is to define the second overload like 
 this:

 void foo(R)(ref R range) if (isInputRange!R && !isSomeString!R 
 && isSomeChar!(ElementType!R))
Is that the exact code? isInputRange checks to see if the type has "front" defined.
 https://github.com/dlang/phobos/blob/v2.072.0/std/range/primitives.d#L162

 Also "string" is just an alias of an array, 
 "immutable(char)[]". So an array should have "front" defined. 
 Can you post more code?
No, an array should not have "front" defined according to D language specification. "front" for arrays is defined in std.range.primitives. That's the problem, I cannot have two specialized functions (one taking arrays and one taking ranges), because std.range.primitives hijacks any char array and transforms it in a range). front, empty, popFront must not be defined for arrays in the same module as isInputRange. Exact code is irrelevant, but you're welcome: string constructor: https://github.com/rumbu13/numerics/blob/master/src/numerics/fixed.d#L978 range constructor https://github.com/rumbu13/numerics/blob/master/src/numerics/fixed.d#L1104
Dec 03 2016
parent reply lobo <swamplobo gmail.com> writes:
On Saturday, 3 December 2016 at 17:29:47 UTC, rumbu wrote:
 On Saturday, 3 December 2016 at 16:37:21 UTC, Jerry wrote:
 On Saturday, 3 December 2016 at 11:52:00 UTC, rumbu wrote:
[...]
[...]
 [...]
Is that the exact code? isInputRange checks to see if the type has "front" defined.
 https://github.com/dlang/phobos/blob/v2.072.0/std/range/primitives.d#L162

 Also "string" is just an alias of an array, 
 "immutable(char)[]". So an array should have "front" defined. 
 Can you post more code?
No, an array should not have "front" defined according to D language specification. "front" for arrays is defined in std.range.primitives. That's the problem, I cannot have two specialized functions (one taking arrays and one taking ranges), because std.range.primitives hijacks any char array and transforms it in a range). front, empty, popFront must not be defined for arrays in the same module as isInputRange. Exact code is irrelevant, but you're welcome: string constructor: https://github.com/rumbu13/numerics/blob/master/src/numerics/fixed.d#L978 range constructor https://github.com/rumbu13/numerics/blob/master/src/numerics/fixed.d#L1104
This works for me when specialising for input ranges, strings and arrays. auto f(T)(T val) if(isInputRange!T && !isSomeString!T && !isArray!T) {} auto f(T)(T val) if(isSomeString!T) {} auto f(T)(T val) if(isArray!T && !isSomeString!T) bye, lobo
Dec 03 2016
parent reply rumbu <rumbu rumbu.ro> writes:
On Sunday, 4 December 2016 at 05:31:59 UTC, lobo wrote:
 This works for me when specialising for input ranges, strings 
 and arrays.

 auto f(T)(T val)
     if(isInputRange!T && !isSomeString!T && !isArray!T) {}

 auto f(T)(T val)
     if(isSomeString!T) {}

 auto f(T)(T val)
     if(isArray!T && !isSomeString!T)

 bye,
 lobo
Yes, this is the same workaround I found, but that does not solve the fact that the following code does not compile: import std.range.primitives : isInputRange; void f(T)(T val) if (isInputRange!T) { while (!val.empty) { auto c = val.front; val.popFront(); } } string s = "some string"; f(s); Of course, the previous code will compile if we change the imports: import std.range.primitives: isInputRange, front, popFront, empty; But that just prove the bad design of isInputRange which cannot be used all alone without the rest of array UFCSs imported.
Dec 04 2016
next sibling parent reply Mike Parker <aldacron gmail.com> writes:
On Sunday, 4 December 2016 at 11:18:56 UTC, rumbu wrote:


 Of course, the previous code will compile if we change the 
 imports:
 import std.range.primitives: isInputRange, front, popFront, 
 empty;

 But that just prove the bad design of isInputRange which cannot 
 be used all alone without the rest of array UFCSs imported.
isInputRange can be used alone just fine. But you aren't using it alone. You're using it together with the range primitives for arrays, but you are explicitly excluding them from the import by only selectively importing isInputRange. The import is working exactly as advertised. What is it about this case that makes you think it should behave differently?
Dec 04 2016
parent reply rumbu <rumbu rumbu.ro> writes:
On Sunday, 4 December 2016 at 11:50:08 UTC, Mike Parker wrote:
 On Sunday, 4 December 2016 at 11:18:56 UTC, rumbu wrote:


 Of course, the previous code will compile if we change the 
 imports:
 import std.range.primitives: isInputRange, front, popFront, 
 empty;

 But that just prove the bad design of isInputRange which 
 cannot be used all alone without the rest of array UFCSs 
 imported.
isInputRange can be used alone just fine. But you aren't using it alone.
I'm using it all alone: auto first(R)(R range) if (isInputRange!R) { return range.front; //compile time error when calling first!string } auto first(C)(const(C)[] str) { return str[0]; //never called; } first(someString);
 You're using it together with the range primitives for arrays, 
 but you are explicitly excluding them from the import by only 
 selectively importing isInputRange. The import is working 
 exactly as advertised. What is it about this case that makes 
 you think it should behave differently?
Advertising from the docs says this: "An input range must define the primitives empty, popFront, and front." In my module context, I didn't define any of them, isInputRange!string must return false, because string is not a range, I repeat - *in my module context*. There is nowhere in the documentation where arrays are advertised as ranges by default. If I copy the isInputRange definition in my module, it will clearly return false: import std.range.primitives: isInputRange; template isInputRange2(R) { enum bool isInputRange2 = is(typeof( (inout int = 0) { R r = R.init; // can define a range object if (r.empty) {} // can test for empty r.popFront(); // can invoke popFront() auto h = r.front; // can get the front of the range })); } static assert(isInputRange!string); //wrong! static assert(!isInputRange2!string); //correct! I think that the array range UFCSs must be moved out from the std.range.primitives and let the library user to decide if there is a need for range semantics applied to all arrays. Otherwise, as long as you want array specializations for your functions, you must decorate all the range specializations with (isInputRange!T && !isArray!T). And you are compelled to use "f(T)(T t) if isArray!T" for all your array specializations instead of "f(T)(T[] x)".
Dec 04 2016
next sibling parent ag0aep6g <anonymous example.com> writes:
On Sunday, 4 December 2016 at 13:37:35 UTC, rumbu wrote:
 There is nowhere in the documentation where arrays are 
 advertised as  ranges by default.
Huh. It really doesn't seem to say so in the prose. It's shown in the examples for isInputRange, though: static assert( isInputRange!(int[])); static assert( isInputRange!(char[]));
 I think that the array range UFCSs must be moved out from the 
 std.range.primitives and let the library user to decide if 
 there is a need for range semantics applied to all arrays.
I'm afraid that's not so easy. std.range.primitives.isInputRange cannot see the range primitives when you import/define them yourself. You'd have to make isInputRange a mixin template, or replace the UFCS primitives with a wrapper type, or something like that. Either way, it'd be a major breaking change. If you're passionate about this, I suggest you try and find a solution with an acceptable migration path. Work out all the annoying little details, and present it as a fleshed-out proposal. I wouldn't bet on it getting accepted, though. The current situation may be slightly irritating, but it may be the lesser evil when compared to breaking everyone's code by messing with arrays as ranges.
 Otherwise, as long as you want array specializations for your 
 functions, you must decorate all the range specializations with 
 (isInputRange!T && !isArray!T). And you are compelled to use 
 "f(T)(T t) if isArray!T" for all your array specializations 
 instead of "f(T)(T[] x)".
You can also use a "specialization" (in the narrower sense of the word that the spec uses): import std.range: isInputRange; import std.stdio: writeln; void f(R)(R r) if (isInputRange!R) { writeln("range"); } void f(A : E[], E)(A a) { writeln("array"); } void main() { int[] a; f(a); } /* calls the second overload; prints "array" */ See https://dlang.org/spec/template.html#parameters_specialization
Dec 04 2016
prev sibling parent Jonathan M Davis via Digitalmars-d <digitalmars-d puremagic.com> writes:
On Sunday, December 04, 2016 13:37:35 rumbu via Digitalmars-d wrote:
 I think that the array range UFCSs must be moved out from the
 std.range.primitives and let the library user to decide if there
 is a need for range semantics applied to all arrays.
That's not happening. In fact, pretty much the only reason that the array range primitives haven't been moved to object.d is because of the auto-decoding issues with strings and the fact that the auto-decoding relies on std.utf in Phobos, whereas object.d is in druntime, which doesn't have access to Phobos. The fact that we currently have to import std.range or std.range.primitives to get arrays to behave properly as ranges is an annoyance that we want to fix, not something that we want to solidify. And other modules - such as std.algorithm - actually publicly import std.range in an attempt to reduce the amount of importing pain with the range primitives. Ranges are a core part of D, and it was never really intended for the range primitives to be optional. They just didn't need to be put into the language or druntime to work, so Andrei put them in Phobos. There are some minor changes that were made to the language (e.g. foreach knowing about the input range primitives), but overall, the range stuff was done via the standard library, because there was no need to do it with the language, and Andrei favors doing stuff with the standard library where possible rather than doing it with the language. So, really, the intention here is that all range based code imports the range primitives for arrays, and there is no plan to support the range-based stuff without them. And there was recently talk of putting the array range primitives in object.d in spite of the auto-decoding mess and just putting more of the std.utf stuff in druntime. So, if anything, that's the direction that things are going, not towards trying to make it possible for code to use isInputRange sanely without the array primitives. The only reason that you're even hitting this issue is because you're explicitly avoiding importing all of the range primitives together and are trying to grab certain ones individually, which was never an intended use case. So, if you're doing that and running into problems, I'm sorry, but you're trying to use the library in a way that it was not designed to be used. - Jonathan M Davis
Dec 04 2016
prev sibling parent reply pineapple <meapineapple gmail.com> writes:
On Sunday, 4 December 2016 at 11:18:56 UTC, rumbu wrote:
 Yes, this is the same workaround I found, but that does not 
 solve the fact that the following code does not compile:
While it may be too late to redeem Phobos and its handling of arrays as ranges, it is worth noting that in the library I've been working on the `isRange` template behaves like you're expecting. In mach, `front` and `popFront` and `empty` are not defined for strings; rather, functions that accept ranges also accept types that ranges can be made to enumerate, including strings and other arrays. If you really wanted an `isInputRange` that behaves like you're wanting, it's only a 6 line template that you would have to interject in your code. enum bool isInputRange(T) = is(typeof({ T range = T.init; if(range.empty){} auto element = range.front; range.popFront(); })); https://github.com/pineapplemachine/mach.d
Dec 05 2016
next sibling parent jmh530 <john.michael.hall gmail.com> writes:
On Monday, 5 December 2016 at 21:51:28 UTC, pineapple wrote:
 https://github.com/pineapplemachine/mach.d
I love that a few of the subfolders have a Readme.md so that I don't have to dig in to anything to get an overview.
Dec 06 2016
prev sibling parent rumbu <rumbu rumbu.ro> writes:
On Monday, 5 December 2016 at 21:51:28 UTC, pineapple wrote:
 On Sunday, 4 December 2016 at 11:18:56 UTC, rumbu wrote:
---
 If you really wanted an `isInputRange` that behaves like you're 
 wanting, it's only a 6 line template that you would have to 
 interject in your code.

     enum bool isInputRange(T) = is(typeof({
         T range = T.init;
         if(range.empty){}
         auto element = range.front;
         range.popFront();
     }));

 https://github.com/pineapplemachine/mach.d
My post was about a wrong design decision which generates a compiler error just with one symbol import. If I didn't know how to solve this, I would probably post in the learn section :) After I read the Jonathan's explanations, I understood that it's my fault, one must guess which is the proper way to use a standard library, so I didn't bother to continue the discussion. Anyway, you have a very nice library that just proves my point that a better design is possible: the "asrange" solution for arrays is very elegant. Finally, for my library I decided to completely ignore the range concept, I just check for isIterable and do my processing in a foreach loop.
Dec 06 2016