View mode: basic / threaded / horizontal-split · Log in · Help
July 28, 2012
Differing levels of type-inference: Can D do this?
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.
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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.
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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.
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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
July 28, 2012
Re: Differing levels of type-inference: Can D do this?
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.
July 29, 2012
Re: Differing levels of type-inference: Can D do this?
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
July 29, 2012
Re: Differing levels of type-inference: Can D do this?
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
« First   ‹ Prev
1 2
Top | Discussion index | About this forum | D home