www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Differing levels of type-inference: Can D do this?

reply Chad J <chadjoan __spam.is.bad__gmail.com> writes:
Is there some way to do something similar to this right now?

void main()
{
   // Differing levels of type-inference:
   int[]       r1 = [1,2,3]; // No type-inference.
   Range!(int) r2 = [1,2,3]; // Only range kind inferred.
   Range       r3 = [1,2,3]; // Element type inferred.
   auto        r4 = [1,2,3]; // Full type-inference.
}

AFAIK it isn't: the type system is pretty much all-or-nothing about 
this.  Please show me that I'm wrong.
Jul 27 2012
next sibling parent reply Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday, July 28, 2012 02:49:16 Chad J wrote:
 Is there some way to do something similar to this right now?
 
 void main()
 {
    // Differing levels of type-inference:
    int[]       r1 = [1,2,3]; // No type-inference.

That works just fine.
    Range!(int) r2 = [1,2,3]; // Only range kind inferred.

What do you mean by range kind? If you declare Range!int, you gave it its type already. It's whatever Range!int is. You can't change it. There's nothing to infer. Either Range!int has a constructor which takes an int[] or the initialization won't work. Range!int is the same either way.
    Range       r3 = [1,2,3]; // Element type inferred.

Again, the type of the variable must already be a full type, or you can't declare it. So, there's nothing to infer. Either Range!int has a constructor which takes an int[] or the initialization won't work. Range is the same either way.
    auto        r4 = [1,2,3]; // Full type-inference.

This works;
 AFAIK it isn't: the type system is pretty much all-or-nothing about
 this.  Please show me that I'm wrong.

The _only_ time that the type on the left-hand side of an assignment expression depends on the type of the right-hand side is if the type is being explicitly inferred by using auto, const, immutable, or enum as the type by themselves. Types are never magically altered by what you assign to them. If you want to, you can create a templated function which picks the type to return. e.g. Type var = makeType([1, 2, 3]); or auto var = makeType([1, 2, 3]); but it's the function which determines what the type is. - Jonathan M Davis
Jul 28 2012
parent reply Chad J <chadjoan __spam.is.bad__gmail.com> writes:
On 07/28/2012 03:03 AM, Jonathan M Davis wrote:
 On Saturday, July 28, 2012 02:49:16 Chad J wrote:
 Is there some way to do something similar to this right now?

 void main()
 {
     // Differing levels of type-inference:
     int[]       r1 = [1,2,3]; // No type-inference.

That works just fine.

Of course. It's provided as a reference for one extreme.
     Range!(int) r2 = [1,2,3]; // Only range kind inferred.

What do you mean by range kind? If you declare Range!int, you gave it its type already. It's whatever Range!int is. You can't change it. There's nothing to infer. Either Range!int has a constructor which takes an int[] or the initialization won't work. Range!int is the same either way.

"range kind" is informal language. Maybe I mean "template instances", but that would somewhat miss the point. I don't know how to do this right now. AFAIK, it's not doable. When I speak of ranges I refer specifically to the std.phobos ranges. There is no Range type right now, but there are the isInputRange, isOutputRange, isForwardRange, etc. templates that define what a Range is. The problem is that I have no idea how to write something like this: isInputRange!___ r2 = [1,2,3].some.complex.expression(); It doesn't make sense. isInputRange!() isn't a type, so how do I constrain what type is returned from some arbitrary expression? So far the only way I know how to do this is to pass it through a template and use template constraints: auto makeSureItsARange(T)(T arg) if ( isInputRange!T ) { return arg; } auto r2 = makeSureItsARange([1,2,3].some.complex.expression()); however, the above is unreasonably verbose and subject to naming whimsy. There also seem to be some wrappers in std.range, but they seem to have caveats and runtime overhead (OOP interfaces imply vtable usage, etc).
     Range       r3 = [1,2,3]; // Element type inferred.

Again, the type of the variable must already be a full type, or you can't declare it. So, there's nothing to infer. Either Range!int has a constructor which takes an int[] or the initialization won't work. Range is the same either way.

What I want to do is constrain that the type of r3 is some kind of range. I don't care what kind of range, it could be a range of integers, a range of floats, an input range, a forward range, and so on. I don't care which, but it has to be a range.
     auto        r4 = [1,2,3]; // Full type-inference.

This works;

Yep. It's the other end of the extreme. What I'm missing is the stuff in the middle.
 AFAIK it isn't: the type system is pretty much all-or-nothing about
 this.  Please show me that I'm wrong.

The _only_ time that the type on the left-hand side of an assignment expression depends on the type of the right-hand side is if the type is being explicitly inferred by using auto, const, immutable, or enum as the type by themselves. Types are never magically altered by what you assign to them. If you want to, you can create a templated function which picks the type to return. e.g. Type var = makeType([1, 2, 3]); or auto var = makeType([1, 2, 3]); but it's the function which determines what the type is. - Jonathan M Davis

which seems like the "makeSureItsARange" solution I mentioned above. It's out of place because traditionally we could constrain the types of things on the parameters and returns of functions in exactly the same manner as we could constrain variable declarations (give or take some storage classes). But with the new notion of structural conformity of types in D, I don't see how we can give variables the same type constraints that function parameters are allowed to use. This seems like the kind of thing that compile-time struct inheritance/interfaces would solve: struct interface InputRange(T) { T property front(); T popFront(); bool property empty(); } struct MyRange(T) : InputRange!T { private T[] payload; T front() { return payload[0]; } T popFront() { payload = payload[1..$]; return payload; } bool property empty() { return payload.length; } ... } // We can tell from looking at the below line that r is // an InputRange!int. If it was "auto" instead, // we'd have no idea without look at the docs, and // we wouldn't be able to localize future type-mismatches // to this location in the scope/file/whatever. InputRange!int r = someFunctionThatReturnsAMyRange([1,2,3]); But I wanted to check and see if there was some way of doing this already.
Jul 28 2012
next sibling parent reply Chad J <chadjoan __spam.is.bad__gmail.com> writes:
On 07/28/2012 04:55 PM, Jonathan M Davis wrote:
 On Saturday, July 28, 2012 16:47:01 Chad J wrote:
 "range kind" is informal language.  Maybe I mean "template instances",
 but that would somewhat miss the point.

 I don't know how to do this right now.  AFAIK, it's not doable.
 When I speak of ranges I refer specifically to the std.phobos ranges.
 There is no Range type right now, but there are the isInputRange,
 isOutputRange, isForwardRange, etc. templates that define what a Range
 is.  The problem is that I have no idea how to write something like this:

 isInputRange!___ r2 = [1,2,3].some.complex.expression();

 It doesn't make sense.  isInputRange!() isn't a type, so how do I
 constrain what type is returned from some arbitrary expression?

Well, if you want a check, then just use static assert. auto r2 = [1,2,3].some.complex.expression(); static assert(isInputRange!(typeof(r2))); The result isn't going to magically become something else just because you want it to, so all that makes sense is specifically checking that its type is what you want, and static assert will do that just fine. This is completely different from template constraints where the constraint can be used to overload functions and generate results of different types depending on what's passed in. With the code above, it's far too late to change any types by the time r2 is created. - Jonathan M Davis

I suppose that works, but it isn't very consistent with how type safety is normally done. Also it's extremely verbose. I'd need a lot of convincing to chose a language that makes me write stuff like this: auto foo = someFunc(); static assert(isInteger!(typeof(foo)); instead of: int foo = someFunc(); I can tolerate this in D because of the obvious difference in power between D's metaprogramming and other's, but it still seems very lackluster compared to what we could have.
Jul 28 2012
parent Chad J <chadjoan __spam.is.bad__gmail.com> writes:
On 07/28/2012 05:55 PM, Jonathan M Davis wrote:
 On Saturday, July 28, 2012 17:48:21 Chad J wrote:
 I suppose that works, but it isn't very consistent with how type safety
 is normally done.  Also it's extremely verbose.  I'd need a lot of
 convincing to chose a language that makes me write stuff like this:

 auto foo = someFunc();
 static assert(isInteger!(typeof(foo));

 instead of:

 int foo = someFunc();

 I can tolerate this in D because of the obvious difference in power
 between D's metaprogramming and other's, but it still seems very
 lackluster compared to what we could have.

Why would you even need to check the return type in most cases? It returns whatever range type returns, and you pass it on to whatever other range-based function you want to use it on, and if a template constraint fails because the type wasn't quite right, _then_ you go and figure out what type of range it returned. But as long as it compiles with the next range-based function, I don't see why it would matter all that much what the exact return type is. auto's inferrence is saving you a lot of trouble (especially when it comes to refactoring). Without it, most range-based stuff would be completely unusable. - Jonathan M Davis

What's missing then is: - A compiler-checked and convenient/readable way of documenting what is being produced by an expression. - A way to localize errors if a 3rd party breaks their API by changing the return type of some function I call. "I want to make sure." - Ease of learning. I would definitely reach for "InputRange r = ..." before reaching for "auto r = ...; static assert (isInputRange!...);". I do love auto. I think it's awesome. I'm just trying to explore what's missing, because there is something bugging me about the current situation. I think it's this problem: Reading code that declares a bunch of variables as "auto" can be disorienting. Sometimes I want to put more specific types in my declarations instead of "auto". This makes things much more readable, in some cases. However, this is very difficult to do now because a bunch of stuff in Phobos returns these voldemort types. I don't know what to replace the "auto" declarations with to make things more readable. I'd at least like some way of specifying the type; a way that is as concise as the expression that yielded the type. It reminds me of the situation in dynamically-typed languages where you're /forced/ to omit type information. It's not as bad here because any eventual mistakes due to type mismatches are still caught at compile-time in D. However, there still seems to be some amount of unavoidable guesswork in the current system. There is something unsettling about this.
Jul 28 2012
prev sibling next sibling parent reply =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 07/28/2012 01:47 PM, Chad J wrote:

 What I want to do is constrain that the type of r3 is some kind of
 range. I don't care what kind of range, it could be a range of integers,
 a range of floats, an input range, a forward range, and so on. I don't
 care which, but it has to be a range.

It does exist in Phobos as inputRangeObject() (and ouputRangeObject). Although the name sounds limiting, inputRangeObject() can present any non-output range as a dynamically-typed range object. This example demonstrates how the programmer wanted an array of ForwardRange!int objects and inputRangeObject supported the need: import std.algorithm; import std.range; import std.stdio; void main() { int[] a1 = [1, 2, 3]; ForwardRange!int r1 = inputRangeObject(map!"2 * a"(a1)); ForwardRange!int r2 = inputRangeObject(map!"a ^^ 2"(a1)); auto a2 = [r1, r2]; writeln(a2); } That works because inputRangeObject uses 'static if' internally to determine what functionality the input range has. Note that r1 and r2 are based on two different original range types as the string delegate that the map() template takes makes the return type unique. The example can be changed like this to add any other ForwardRange!int to the existing ranges collection: auto a2 = [r1, r2]; a2 ~= inputRangeObject([10, 20]); writeln(a2); Ali
Jul 29 2012
parent reply Chad J <chadjoan __spam.is.bad__gmail.com> writes:
On 07/29/2012 11:54 AM, Ali Çehreli wrote:
 On 07/28/2012 01:47 PM, Chad J wrote:

  > What I want to do is constrain that the type of r3 is some kind of
  > range. I don't care what kind of range, it could be a range of integers,
  > a range of floats, an input range, a forward range, and so on. I don't
  > care which, but it has to be a range.

 It does exist in Phobos as inputRangeObject() (and ouputRangeObject).
 Although the name sounds limiting, inputRangeObject() can present any
 non-output range as a dynamically-typed range object.

 This example demonstrates how the programmer wanted an array of
 ForwardRange!int objects and inputRangeObject supported the need:

 import std.algorithm;
 import std.range;
 import std.stdio;

 void main() {
 int[] a1 = [1, 2, 3];

 ForwardRange!int r1 = inputRangeObject(map!"2 * a"(a1));
 ForwardRange!int r2 = inputRangeObject(map!"a ^^ 2"(a1));

 auto a2 = [r1, r2];

 writeln(a2);
 }

 That works because inputRangeObject uses 'static if' internally to
 determine what functionality the input range has.

 Note that r1 and r2 are based on two different original range types as
 the string delegate that the map() template takes makes the return type
 unique.

 The example can be changed like this to add any other ForwardRange!int
 to the existing ranges collection:

 auto a2 = [r1, r2];
 a2 ~= inputRangeObject([10, 20]);
 writeln(a2);

 Ali

IIRC, these are classes that come with all the typical runtime overhead, right? I intend to try and keep the awesome mix of potentially optimal code that's also completely generalized. Introducing hard-to-inline vtable calls into the mix would run against that goal. If not for that, it would be close to what I'm looking for, minus the extraneous function call tacked onto every expression (which also makes it less discoverable).
Jul 29 2012
parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 07/29/2012 10:22 AM, Chad J wrote:
 On 07/29/2012 11:54 AM, Ali Çehreli wrote:

 ForwardRange!int r1 = inputRangeObject(map!"2 * a"(a1));
 ForwardRange!int r2 = inputRangeObject(map!"a ^^ 2"(a1));


 IIRC, these are classes that come with all the typical runtime overhead,
 right?

Yes, inputRangeObject() allows runtime polymorphism over compile-time polymorphism.
 I intend to try and keep the awesome mix of potentially optimal code
 that's also completely generalized. Introducing hard-to-inline vtable
 calls into the mix would run against that goal.

Yes, usual runtime vs. compile-time polymorphism considerations apply. Ali
Jul 29 2012
prev sibling parent Chad J <chadjoan __spam.is.bad__gmail.com> writes:
On 07/29/2012 08:32 AM, Simen Kjaeraas wrote:
 On Sat, 28 Jul 2012 22:47:01 +0200, Chad J
 <chadjoan __spam.is.bad__gmail.com> wrote:


 isInputRange!___ r2 = [1,2,3].some.complex.expression();

 It doesn't make sense. isInputRange!() isn't a type, so how do I
 constrain what type is returned from some arbitrary expression?

 So far the only way I know how to do this is to pass it through a
 template and use template constraints:

 auto makeSureItsARange(T)(T arg) if ( isInputRange!T )
 {
 return arg;
 }

 auto r2 = makeSureItsARange([1,2,3].some.complex.expression());

 however, the above is unreasonably verbose and subject to naming whimsy.

import std.typetuple : allSatisfy; template checkConstraint( T ) { template checkConstraint( alias Constraint ) { enum checkConstraint = Constraint!T; } } template constrain( T... ) { auto constrain( U, string file = __FILE__, int line = __LINE__ )( auto ref U value ) { static assert( allSatisfy!( checkConstraint!U, T ), "Type " ~ U.stringof ~ " does not fulfill the constraints " ~ T.stringof ); return value; } } version (unittest) { import std.range : isInputRange, ElementType; template hasElementType( T ) { template hasElementType( U ) { enum hasElementType = is( ElementType!U == T ); } } } unittest { assert( __traits( compiles, { int[] a = constrain!isInputRange( [1,2,3] ); } ) ); assert( !__traits( compiles, { int a = constrain!isInputRange( 2 ); } ) ); assert( __traits( compiles, { int[] a = constrain!(isInputRange, hasElementType!int)( [1,2,3] ); } ) ); assert( __traits( compiles, { string a = constrain!(isInputRange, hasElementType!dchar)( "abc" ); } ) ); assert( !__traits( compiles, { string a = constrain!(isInputRange, hasElementType!dchar)( "abc"w ); } ) ); } So there. Now, you simply use auto a = constrain!isInputRange( expression );. Is this what you wanted?

That's pretty good. It's still not as concise or easy to discover as the language's natural syntax for type declarations, but it's the kind of thing I'd use for my own purposes as a trick to get around language limitations.
Jul 29 2012
prev sibling next sibling parent =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 07/27/2012 11:49 PM, Chad J wrote:

 Range r3 = [1,2,3]; // Element type inferred.

If you mean that you wanted a Range!int on the left-hand side, unfortunately there is no template type deduction for struct and class templates. On the other hand, there is type deduction for function templates and that is the reason for the common approach of providing a convenient function along with struct and class templates: struct Range(T) { this(T[] slice) {} } Range!T range(T)(T[] args) { return Range!T(args); } void main() { auto r = range([1,2,3]); assert(typeid(r) == typeid(Range!int)); } Ali
Jul 28 2012
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday, July 28, 2012 16:47:01 Chad J wrote:
 "range kind" is informal language.  Maybe I mean "template instances",
 but that would somewhat miss the point.
 
 I don't know how to do this right now.  AFAIK, it's not doable.
 When I speak of ranges I refer specifically to the std.phobos ranges.
 There is no Range type right now, but there are the isInputRange,
 isOutputRange, isForwardRange, etc. templates that define what a Range
 is.  The problem is that I have no idea how to write something like this:
 
 isInputRange!___ r2 = [1,2,3].some.complex.expression();
 
 It doesn't make sense.  isInputRange!() isn't a type, so how do I
 constrain what type is returned from some arbitrary expression?

Well, if you want a check, then just use static assert. auto r2 = [1,2,3].some.complex.expression(); static assert(isInputRange!(typeof(r2))); The result isn't going to magically become something else just because you want it to, so all that makes sense is specifically checking that its type is what you want, and static assert will do that just fine. This is completely different from template constraints where the constraint can be used to overload functions and generate results of different types depending on what's passed in. With the code above, it's far too late to change any types by the time r2 is created. - Jonathan M Davis
Jul 28 2012
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday, July 28, 2012 17:48:21 Chad J wrote:
 I suppose that works, but it isn't very consistent with how type safety
 is normally done.  Also it's extremely verbose.  I'd need a lot of
 convincing to chose a language that makes me write stuff like this:
 
 auto foo = someFunc();
 static assert(isInteger!(typeof(foo));
 
 instead of:
 
 int foo = someFunc();
 
 I can tolerate this in D because of the obvious difference in power
 between D's metaprogramming and other's, but it still seems very
 lackluster compared to what we could have.

Why would you even need to check the return type in most cases? It returns whatever range type returns, and you pass it on to whatever other range-based function you want to use it on, and if a template constraint fails because the type wasn't quite right, _then_ you go and figure out what type of range it returned. But as long as it compiles with the next range-based function, I don't see why it would matter all that much what the exact return type is. auto's inferrence is saving you a lot of trouble (especially when it comes to refactoring). Without it, most range-based stuff would be completely unusable. - Jonathan M Davis
Jul 28 2012
prev sibling next sibling parent "Simen Kjaeraas" <simen.kjaras gmail.com> writes:
On Sat, 28 Jul 2012 22:47:01 +0200, Chad J  
<chadjoan __spam.is.bad__gmail.com> wrote:


 isInputRange!___ r2 = [1,2,3].some.complex.expression();

 It doesn't make sense.  isInputRange!() isn't a type, so how do I  
 constrain what type is returned from some arbitrary expression?

 So far the only way I know how to do this is to pass it through a  
 template and use template constraints:

 auto makeSureItsARange(T)(T arg) if ( isInputRange!T )
 {
 	return arg;
 }

 auto r2 = makeSureItsARange([1,2,3].some.complex.expression());

 however, the above is unreasonably verbose and subject to naming whimsy.

import std.typetuple : allSatisfy; template checkConstraint( T ) { template checkConstraint( alias Constraint ) { enum checkConstraint = Constraint!T; } } template constrain( T... ) { auto constrain( U, string file = __FILE__, int line = __LINE__ )( auto ref U value ) { static assert( allSatisfy!( checkConstraint!U, T ), "Type " ~ U.stringof ~ " does not fulfill the constraints " ~ T.stringof ); return value; } } version (unittest) { import std.range : isInputRange, ElementType; template hasElementType( T ) { template hasElementType( U ) { enum hasElementType = is( ElementType!U == T ); } } } unittest { assert( __traits( compiles, { int[] a = constrain!isInputRange( [1,2,3] ); } ) ); assert( !__traits( compiles, { int a = constrain!isInputRange( 2 ); } ) ); assert( __traits( compiles, { int[] a = constrain!(isInputRange, hasElementType!int)( [1,2,3] ); } ) ); assert( __traits( compiles, { string a = constrain!(isInputRange, hasElementType!dchar)( "abc" ); } ) ); assert( !__traits( compiles, { string a = constrain!(isInputRange, hasElementType!dchar)( "abc"w ); } ) ); } So there. Now, you simply use auto a = constrain!isInputRange( expression );. Is this what you wanted? -- Simen
Jul 29 2012
prev sibling parent "Simen Kjaeraas" <simen.kjaras gmail.com> writes:
On Sun, 29 Jul 2012 19:11:17 +0200, Chad J  
<chadjoan __spam.is.bad__gmail.com> wrote:

 So there. Now, you simply use auto a = constrain!isInputRange(
 expression );. Is this what you wanted?

That's pretty good. It's still not as concise or easy to discover as the language's natural syntax for type declarations, but it's the kind of thing I'd use for my own purposes as a trick to get around language limitations.

It's likely as good as it gets without changing the language. Of course, if you use this a lot with the same predicate, you could alias it to something shorter: alias constrain!isInputRange InputRange; auto a = InputRange(expression); Note also that the implementation supports multiple predicates, hence the supplied hasElementType. And this is where aliases really come in handy: template hasElementType( T ) { template hasElementType( U ) { enum hasElementType = is( ElementType!U == T ); } } alias constrain!(isInputRange, hasElementType!int) IntRange; auto a = IntRange([1,2,3]); -- Simen
Jul 29 2012