Thread overview | |||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
May 08, 2014 From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
So there's this recent discussion about making T[] be refcounted if and only if T has a destructor. That's an interesting idea. More generally, there's the notion that making user-defined types as powerful as built-in types is a Good Thing(tm). Which brings us to something that T[] has that user-defined types cannot have. Consider: import std.stdio; void fun(T)(T x) { writeln(typeof(x).stringof); } void main() { immutable(int[]) a = [ 1, 2 ]; writeln(typeof(a).stringof); fun(a); } This program outputs: immutable(int[]) immutable(int)[] which means that the type of that value has subtly and silently changed in the process of passing it to a function. This change was introduced a while ago (by Kenji I recall) and it enabled a lot of code that was gratuitously rejected. This magic of T[] is something that custom ranges can't avail themselves of. In order to bring about parity, we'd need to introduce opByValue which (if present) would be automatically called whenever the object is passed by value into a function. This change would allow library designers to provide good solutions to making immutable and const ranges work properly - the way T[] works. There are of course a bunch of details to think about and figure out, and this is a large change. Please chime in with thoughts. Thanks! Andrei |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | This looks like interesting. |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On 8.5.2014. 5:58, Andrei Alexandrescu wrote: > > This magic of T[] is something that custom ranges can't avail themselves of. In order to bring about parity, we'd need to introduce opByValue which (if present) would be automatically called whenever the object is passed by value into a function. > > This change would allow library designers to provide good solutions to making immutable and const ranges work properly - the way T[] works. > Looks very similar to some kind of opImplicitConvert. http://forum.dlang.org/thread/teddgvbtmrxumffrhojh@forum.dlang.org http://forum.dlang.org/thread/gq0fj7$4av$1@digitalmars.com Maybe it would be better to have a more general solution instead of special case solution, if there is no reason against implicit conversion of course. |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | Andrei Alexandrescu: > making user-defined types as powerful as built-in types is a Good Thing(tm). An example of something useful that I think is not currently easy to do with user-defined types (but I think this could be done by future built-in tuples): Tuple!(ref int, bool) foo(ref int x) pure { x++; return tuple(x, true); } > In order to bring about parity, we'd need to > introduce opByValue which (if present) would be automatically called whenever the object is passed by value into a function. I suggest to add some usage examples, to help focus the discussion. Currently only the slices decay in mutables, while an immutable int doesn't become mutable: import std.stdio; void foo(T)(T x) { writeln(typeof(x).stringof); } void main() { immutable a = [1, 2]; writeln(typeof(a).stringof); foo(a); immutable y = 10; foo(y); } Output: immutable(int[]) immutable(int)[] immutable(int) Bye, bearophile |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On Wed, 07 May 2014 20:58:21 -0700
Andrei Alexandrescu via Digitalmars-d <digitalmars-d@puremagic.com>
wrote:
> So there's this recent discussion about making T[] be refcounted if and only if T has a destructor.
>
> That's an interesting idea. More generally, there's the notion that making user-defined types as powerful as built-in types is a Good Thing(tm).
>
> Which brings us to something that T[] has that user-defined types cannot have. Consider:
>
> import std.stdio;
>
> void fun(T)(T x)
> {
> writeln(typeof(x).stringof);
> }
>
> void main()
> {
> immutable(int[]) a = [ 1, 2 ];
> writeln(typeof(a).stringof);
> fun(a);
> }
>
> This program outputs:
>
> immutable(int[])
> immutable(int)[]
>
> which means that the type of that value has subtly and silently changed in the process of passing it to a function.
>
> This change was introduced a while ago (by Kenji I recall) and it enabled a lot of code that was gratuitously rejected.
>
> This magic of T[] is something that custom ranges can't avail themselves of. In order to bring about parity, we'd need to introduce opByValue which (if present) would be automatically called whenever the object is passed by value into a function.
>
> This change would allow library designers to provide good solutions to making immutable and const ranges work properly - the way T[] works.
>
> There are of course a bunch of details to think about and figure out, and this is a large change. Please chime in with thoughts. Thanks!
As far as I can see, opByValue does the same thing as opSlice, except that it's used specifically when passing to functions, whereas this code
immutable int [] a = [1, 2, 3];
immutable(int)[] b = a[];
or even
immutable int [] a = [1, 2, 3];
immutable(int)[] b = a;
compiles just fine. So, I don't see how adding opByValue helps us any. Simply calling opSlice implicitly for user-defined types in the same places that it's called implicitly on arrays would solve that problem. We may even do some of that already, though I'm not sure.
The core problem in either case is that const(MyStruct!T) has no relation to MyStruct!(const T) or even const(MyStruct!(const T)). They're different template instantations and therefore can have completely different members. So, attempts to define opSlice such that it returns a tail-const version of the range tends to result in recursive template instantiations which then blow the stack (or maybe error out due to too many levels - I don't recall which at the moment - but regardless, it fails). I think that careful and clever use of static ifs could resolve that, but that's not terribly pleasant. At best, it would result in an idiom that everyone would have to look up exactly how to do correctly every time they needed to define opSlice. Right now, you'd have to declare something like
struct MyRange(T)
{
...
static if(isMutable!T)
MyRange!(const T) opSlice() const {...}
else
MyRange opSlice() const {...}
...
}
and I'm not even sure that that quite works, since I haven't even attempted to define a tail-const opSlice recently. Whereas ideally, you'd just do something mroe like
struct MyRange(T)
{
...
MyRange!(const T) opSlice() const {...}
...
}
but that doesn't currently work due to recursive template instantations.
I don't know quite how we can make it work (maybe making the compiler detect when MyRange!T and MyRange!(const T) are effectively identical), but I think that that's really the problem that we need to solve, not coming up with a new function, because opSlice is already there to do what we need (though it may need to have some additional implicit calls added to it to make it match when arrays are implicitly sliced).
Regardless, I concur that this is a problem that sorely needs solving it. Without it, const and ranges really don't mix at all.
- Jonathan M Davis
|
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to bearophile | On Thu, 08 May 2014 06:48:57 +0000
bearophile via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
> Currently only the slices decay in mutables, while an immutable int doesn't become mutable:
That's because what's happening is that the slice operator for arrays is defined to return a tail-const slice of the array, and then any time you slice it - be it explicit or implicit - you get a tail-const slice. It really has nothing to do with passing an argument to a function beyond the fact that that triggers an implicit call to the slice operator. For feature parity here, what we really should be looking it is how to make opSlice have feature parity, not adding a new function. And ints can't be sliced, so there is no situation where you end up with a tail-const slice of an int.
- Jonathan M Davis
|
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | Just a general note: This is not only interesting for range/slice types, but for any user defined reference type (e.g. RefCounted!T or Isolated!T). |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On 05/08/2014 05:58 AM, Andrei Alexandrescu wrote: > ... > > > import std.stdio; > > void fun(T)(T x) > { > writeln(typeof(x).stringof); > } > > void main() > { > immutable(int[]) a = [ 1, 2 ]; > writeln(typeof(a).stringof); > fun(a); > } > > This program outputs: > > immutable(int[]) > immutable(int)[] > > which means that the type of that value has subtly and silently changed in the process of passing it to a function. > ... This way of stating it is imprecise: What happened is that during implicit function template instantiation, T was determined to be immutable(int)[] instead of immutable(int[]). Then 'a' was implicitly converted from immutable(int[]) to immutable(int)[] just as it would be done for any function call with those argument and parameter types. > This change was introduced a while ago (by Kenji I recall) and it enabled a lot of code that was gratuitously rejected. > This magic of T[] is something that custom ranges can't avail themselves > of. In order to bring about parity, we'd need to introduce opByValue > which (if present) would be automatically called whenever the object is > passed by value into a function. > ... There are two independent pieces of magic here, and this proposal removes none of them in a satisfactory way: struct S(T){ HeadUnqual!(typeof(this)) opByValue(){ ... } ... } void fun(in S!int a){} void main(){ const S!int s; fun(s); // error, cannot implicitly convert expression s.opByValue // of type S!(const(int)) to const(S!int). } A better ad-hoc way of resolving this is opImplicitCast and a special opIFTIdeduce member or something like that. struct S(T){ alias opIFTIdeduce = HeadUnqual!(typeof(this)); S opImplicitCast(S)(typeof(this) arg){ ... } ... } But this just means that now every author of a datatype of suitable kind has to manually re-implement implicit conversion rules of T[]. A probably even better way is to just allow to specify by annotation that some templated datatype should mimic T[]'s implicit conversion rules (and without necessarily providing a direct and overpowered hook into the IFTI resolution process, though both could be done.) > This change would allow library designers to provide good solutions to > making immutable and const ranges work properly - the way T[] works. > ... Questionable. const(T)[] b = ...; const(T[]) a = b; // =) Range!(const(T)) s = ...; const(Range!T) r = s; // =( |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Andrei Alexandrescu | On 2014-05-08 03:58:21 +0000, Andrei Alexandrescu <SeeWebsiteForEmail@erdani.org> said: > So there's this recent discussion about making T[] be refcounted if and only if T has a destructor. > > That's an interesting idea. More generally, there's the notion that making user-defined types as powerful as built-in types is a Good Thing(tm). > > ... > > This magic of T[] is something that custom ranges can't avail themselves of. In order to bring about parity, we'd need to introduce opByValue which (if present) would be automatically called whenever the object is passed by value into a function. Will this solve the problem that const(MyRange!(const T)) is a different type from const(MyRange!(T))? I doubt it. But they should be the same type if we want to follow the semantics of the language's slices, where const(const(T)[]) is the same as const(T[]). Perhaps this is an orthogonal issue, but I wonder whether a solution to the above problem could make opByValue unnecessary. -- Michel Fortin michel.fortin@michelf.ca http://michelf.ca |
May 08, 2014 Re: From slices to perfect imitators: opByValue | ||||
---|---|---|---|---|
| ||||
Posted in reply to Michel Fortin | On 05/08/2014 12:14 PM, Michel Fortin wrote: > > Will this solve the problem that const(MyRange!(const T)) is a different > type from const(MyRange!(T))? No, but as stated it aggravates this problem. > I doubt it. But they should be the same > type if we want to follow the semantics of the language's slices, where > const(const(T)[]) is the same as const(T[]). > > Perhaps this is an orthogonal issue, but I wonder whether a solution to > the above problem could make opByValue unnecessary. Not necessarily automatically, because there would still need to be a way to figure out that actually const(S!T) -> S!(const(T)) is the way to remove top-level constness. (Because sometimes it is actually const(S!(T[])) -> S!(const(T)[]), for example, for most ranges in std.algorithm.) But I think the above problem is the fundamental one. |
Copyright © 1999-2021 by the D Language Foundation