www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - enums and std.traits

reply Jonathan M Davis <jmdavisProg gmx.com> writes:
At present (though it didn't used to work this way until about 4 months ago - 
the change is in 2.060), a number of std.traits templates - including 
isSomeString - evaluate to false for enums. So,

enum E : string { a = "hello" }
static assert(isSomeString!(E.a));

will fail. This is in spite of the fact that

auto func(string a) {...}

will accept E.a without complaint. So, if you change

auto func(string a) {...}

to

auto func(T)(T a) if(isSomeString!T) {...}

any code passing E.a to func would be broken. Personally, I don't see why 
isSomeString shouldn't consider enums which are strings to be strings (the 
same goes for isUnsigned, isIntegral, etc.), especially when the language 
itself does.

Kenji is argues that they shouldn't because they should treat enums as 
strongly typed:

https://github.com/D-Programming-
Language/phobos/commit/52462bec6ea846f30e8dac309d63ccb4101e8f0c#commitcomment-1671599

But the language itself doesn't and I think that this a real problem given 
that it's very common to use stuff like isSomeString in template constraints, 
and I don't think that much of anyone is considering how that effects enums.

I agree that an enum's base type should not implicitly convert to the enum 
type, since that would mean that you could have an invalid enum value, but I 
see no problem with enums implicitly converted to their base type.

I think that the change to std.traits was a big mistake and will probably file 
it as a regression, but I thought that I should get other people's thoughts on 
this.

- Jonathan M Davis
Aug 04 2012
next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 08/04/2012 11:09 PM, Jonathan M Davis wrote:
 I agree that an enum's base type should not implicitly convert to the enum
 type, since that would mean that you could have an invalid enum value, but I
 see no problem with enums implicitly converted to their base type.

Well, there is a problem: T fun(T)(T arg) if(isSomeString!arg){ return arg~arg[0]; } The constraint would be too weak.
Aug 04 2012
parent reply travert phare.normalesup.org (Christophe Travert) writes:
Jonathan M Davis , dans le message (digitalmars.D:174267), a écrit :
 On Saturday, August 04, 2012 15:22:34 Jonathan M Davis wrote:
 On Sunday, August 05, 2012 00:15:02 Timon Gehr wrote:
 T fun(T)(T arg) if(isSomeString!arg){
 
      return arg~arg[0];
 
 }



IMO, the behavior should be this: when trying to call the template with a argument that is an enum type based on string, the compiler should try to instanciate the template for this enum type, and isSomeString should fail. Then, the the compiler will try to instanciate the template for strings, which works. Thus, the template is called with a string argument, which is the enum converted to a string.
Aug 05 2012
next sibling parent travert phare.normalesup.org (Christophe Travert) writes:
Jonathan M Davis , dans le message (digitalmars.D:174310), a écrit :
 IMO, the behavior should be this: when trying to call the template with
 a argument that is an enum type based on string, the compiler should try
 to instanciate the template for this enum type, and isSomeString should
 fail. Then, the the compiler will try to instanciate the template for
 strings, which works. Thus, the template is called with a string
 argument, which is the enum converted to a string.

I don't believe that the compiler ever tries twice to instantiate _any_ template. It has a specific type that it uses to instantiate the template (usually the exact type passed or the exact type of the value passed in the case of IFTI - but in the case of IFTI and arrays, it's the tail const version of the type that's used rather than the exact type). If it works, then the template is instantiated. If it fails, then it doesn't. There are no second tries with variations on the type which it could be implicitly converted to. And honestly, I think that doing that would just make it harder to figure out what's going on when things go wrong with template instantiations. It would be like how C++ will do up to 3 implicit conversions when a function is called, so you don't necessarily know what type is actually being passed to the function ultimately. It can be useful at times, but it can also be very annoying. D explicitly did not adopt that sort of behavior, and trying multiple types when instantiating a template would not be in line with that decision.

If someone implements a library function taking a string. People start to use that function with an enum based on string, which is fine, since enum implicitely cast to its base type. Now the library writer found a way to make is library more generic, and templatise its function to take any dchar range. All codes using enum instead of string breaks. Or there may be pressure on the library implementer to add load of template specializations to make the template work with enums. There is something wrong here: enum works for string function, but not ones that are template. It forces the user to check it the function he wants to use is a template before trying to use it with something that implicitely cast to the function's argument type. This is a problem that can be avoided by trying to instanciate the template with types that the argument implicitely cast to. Of course, as you stated, mess can arise, because you don't know right away what template instanciation is going to be used. But there would be much less mess than in C++. First, D has a more conservative approach to implicit casting than C++. If an implicit casting is used, it will be one that is visible in the type's declaration, and that the type implementer wanted. The problems would be much more controled than for C++. Second, D has powerful template guards. You can make sure the argument's type given to the template is a of a kind that will work correctly for this function. I don't thing the mess would be huge. Particularly for enums, which are more manifest constants than specific types in D. -- Christophe
Aug 06 2012
prev sibling parent deadalnix <deadalnix gmail.com> writes:
Le 05/08/2012 14:38, Christophe Travert a écrit :
 Jonathan M Davis , dans le message (digitalmars.D:174267), a écrit :
 On Saturday, August 04, 2012 15:22:34 Jonathan M Davis wrote:
 On Sunday, August 05, 2012 00:15:02 Timon Gehr wrote:
 T fun(T)(T arg) if(isSomeString!arg){

       return arg~arg[0];

 }



IMO, the behavior should be this: when trying to call the template with a argument that is an enum type based on string, the compiler should try to instanciate the template for this enum type, and isSomeString should fail. Then, the the compiler will try to instanciate the template for strings, which works. Thus, the template is called with a string argument, which is the enum converted to a string.

With multiple template parameter, it becomes a nightmare for the compiler. The example above is easily solved using auto for return type inference instead of specifying it.
Aug 06 2012
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, August 05, 2012 00:15:02 Timon Gehr wrote:
 On 08/04/2012 11:09 PM, Jonathan M Davis wrote:
 I agree that an enum's base type should not implicitly convert to the enum
 type, since that would mean that you could have an invalid enum value, but
 I see no problem with enums implicitly converted to their base type.

T fun(T)(T arg) if(isSomeString!arg){ return arg~arg[0]; } The constraint would be too weak.

True, but that's still going to cause a lot fewer problems than making isSomeString fail for enums will, and if auto is used, then it's not a problem. Though honestly, I'd argue that the compiler should treat arg ~ arg[0] as being string rather than the enum type anyway, since it makes no sense to get a new enum value by concatenating to an existing one. The new one probably isn't a valid enum value. It's the same problem that we get when we allow bit manipulation on enum types (which should also be disallowed IMHO). So, I think that that's more of a problem with enums than isSomeString. - Jonathan M Davis
Aug 04 2012
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Saturday, August 04, 2012 15:22:34 Jonathan M Davis wrote:
 On Sunday, August 05, 2012 00:15:02 Timon Gehr wrote:
 T fun(T)(T arg) if(isSomeString!arg){
 
      return arg~arg[0];
 
 }


 Though honestly, I'd argue that the compiler should treat arg ~ arg[0] as
 being string rather than the enum type anyway, since it makes no sense to
 get a new enum value by concatenating to an existing one. The new one
 probably isn't a valid enum value. It's the same problem that we get when
 we allow bit manipulation on enum types (which should also be disallowed
 IMHO). So, I think that that's more of a problem with enums than
 isSomeString.

Okay. I responded to that too quickly and mixed myself up a bit. The compiler already behaves this way. It unfortunately does type return arg ~ arg; as the enum type, but it correctly marks return arg ~ arg[0]; as being a string, which does result in breakage. But the alternative right now is that using isSomeString breaks _all_ cases where an enum is passed rather than just some. So, no, having isSomeString work with enums isn't perfect, but I still think that it's better than the current situation. - Jonathan M Davis
Aug 04 2012
prev sibling next sibling parent "Adam D. Ruppe" <destructionator gmail.com> writes:
On Saturday, 4 August 2012 at 21:09:40 UTC, Jonathan M Davis 
wrote:
 I think that the change to std.traits was a big mistake and 
 will probably file it as a regression

I don't know what the right answer here is, but if you do end up reverting those changes, be sure to change std.conv.to too so that bug which led to this change doesn't come back. Probably adding is(T == enum) and !is(T==enum) to the appropriate places will do the trick.
Aug 04 2012
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, August 05, 2012 05:40:40 Adam D. Ruppe wrote:
 On Saturday, 4 August 2012 at 21:09:40 UTC, Jonathan M Davis
 
 wrote:
 I think that the change to std.traits was a big mistake and
 will probably file it as a regression

I don't know what the right answer here is, but if you do end up reverting those changes, be sure to change std.conv.to too so that bug which led to this change doesn't come back. Probably adding is(T == enum) and !is(T==enum) to the appropriate places will do the trick.

Oh. Definitely. And if the appropriate unit tests are there (as they should be), then changing isSomeString should actually break them until those functions are fixed to take the isSomeString changes into account. But I'd much rather have the rare function which cares about differentiating between enums and their base types (e.g. std.conv.to) checks for that itself that make every function which doesn't care do the checking. - Jonathan M Davis
Aug 04 2012
prev sibling next sibling parent Jonathan M Davis <jmdavisProg gmx.com> writes:
On Sunday, August 05, 2012 12:38:56 Christophe Travert wrote:
 Jonathan M Davis , dans le message (digitalmars.D:174267), a =C3=A9cr=

 On Saturday, August 04, 2012 15:22:34 Jonathan M Davis wrote:
 On Sunday, August 05, 2012 00:15:02 Timon Gehr wrote:
 T fun(T)(T arg) if(isSomeString!arg){
=20
      return arg~arg[0];
=20
 }



IMO, the behavior should be this: when trying to call the template wi=

 a argument that is an enum type based on string, the compiler should =

 to instanciate the template for this enum type, and isSomeString shou=

 fail. Then, the the compiler will try to instanciate the template for=

 strings, which works. Thus, the template is called with a string
 argument, which is the enum converted to a string.

I don't believe that the compiler ever tries twice to instantiate _any_= =20 template. It has a specific type that it uses to instantiate the templa= te=20 (usually the exact type passed or the exact type of the value passed in= the=20 case of IFTI - but in the case of IFTI and arrays, it's the tail const = version=20 of the type that's used rather than the exact type). If it works, then = the=20 template is instantiated. If it fails, then it doesn't. There are no se= cond=20 tries with variations on the type which it could be implicitly converte= d to. And honestly, I think that doing that would just make it harder to figu= re out=20 what's going on when things go wrong with template instantiations. It w= ould be=20 like how C++ will do up to 3 implicit conversions when a function is ca= lled,=20 so you don't necessarily know what type is actually being passed to the= =20 function ultimately. It can be useful at times, but it can also be very= =20 annoying. D explicitly did not adopt that sort of behavior, and trying=20= multiple types when instantiating a template would not be in line with = that=20 decision. - Jonathan M Davis
Aug 05 2012
prev sibling parent deadalnix <deadalnix gmail.com> writes:
Le 04/08/2012 23:09, Jonathan M Davis a écrit :
 At present (though it didn't used to work this way until about 4 months ago -
 the change is in 2.060), a number of std.traits templates - including
 isSomeString - evaluate to false for enums. So,

 enum E : string { a = "hello" }
 static assert(isSomeString!(E.a));

 will fail. This is in spite of the fact that

 auto func(string a) {...}

 will accept E.a without complaint. So, if you change

 auto func(string a) {...}

 to

 auto func(T)(T a) if(isSomeString!T) {...}

 - Jonathan M Davis

E;A is a string. By definition. So isSomeString must be true. However, T foo(T)(T t) if (isSomeString!T) { return t ~ t[0]; } must trigger an error when T is an enum type. if t has type E, which implicitly cast to string, t ~ t[0] must have type string and not E. So the given sample code should trigger a compile time error about inconsistent return type. The same goes for other enums operations.
Aug 06 2012