Jump to page: 1 2
Thread overview
Differing levels of type-inference: Can D do this?
Jul 28, 2012
Chad J
Jul 28, 2012
Jonathan M Davis
Jul 28, 2012
Chad J
Jul 28, 2012
Jonathan M Davis
Jul 28, 2012
Chad J
Jul 28, 2012
Jonathan M Davis
Jul 28, 2012
Chad J
Jul 29, 2012
Simen Kjaeraas
Jul 29, 2012
Chad J
Jul 29, 2012
Simen Kjaeraas
Jul 29, 2012
Ali Çehreli
Jul 29, 2012
Chad J
Jul 29, 2012
Ali Çehreli
Jul 28, 2012
Ali Çehreli
July 28, 2012
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
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
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
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
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
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
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
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
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
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