Re: Differing levels of type-inference: Can D do this?

2012-07-29 Thread Ali Çehreli

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



Re: Differing levels of type-inference: Can D do this?

2012-07-29 Thread Chad J

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)( abcw ); } ) );
}


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.


Re: Differing levels of type-inference: Can D do this?

2012-07-29 Thread Chad J

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).


Re: Differing levels of type-inference: Can D do this?

2012-07-29 Thread Simen Kjaeraas
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


Re: Differing levels of type-inference: Can D do this?

2012-07-29 Thread Ali Çehreli

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



Re: Differing levels of type-inference: Can D do this?

2012-07-28 Thread Jonathan M Davis
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.

autor4 = [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


Re: Differing levels of type-inference: Can D do this?

2012-07-28 Thread Ali Çehreli

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



Re: Differing levels of type-inference: Can D do this?

2012-07-28 Thread Chad J

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.



autor4 = [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.


Re: Differing levels of type-inference: Can D do this?

2012-07-28 Thread Jonathan M Davis
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


Re: Differing levels of type-inference: Can D do this?

2012-07-28 Thread Chad J

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.


Re: Differing levels of type-inference: Can D do this?

2012-07-28 Thread Chad J

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.