www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - An idiom for disabling implicit conversions

reply Don <nospam nospam.com> writes:
I've found a pretty cool idiom for disabling pesky implicit conversion.

This is a situation that I've encountered with BigInt; but I think it's 
a general problem.
I have an "add integer" operation. Let's call it:
void add( x ), where x is a built-in integral type.

If I define add(long x), everything works great -- it can be called with 
x as a byte, ubyte, short, ushort, int, uint, or long.
Well, almost. Unfortunately, it doesn't work for ulong: add(ulong.max) 
will either fail to compile, or get implicitly cast to add(-1)!!

You can fix this by defining add(ulong x) as a special case. Now x can 
be long or ulong, and it works. Great!
Problem is, now if x is an integer, the compiler complains that it's 
ambiguous -- should it call the long or the ulong version? Bummer!

You could solve that by creating int, uint, short, ushort,... functions. 
Eight in total, but seven of them are identical. Massive source code 
duplication. So use a template:

void add(T)(T x)
{
   // everything except ulong
}

void add(T : ulong)(T x)
{
}

Problem solved! Well, not quite. Now the source code duplication is 
gone, but you still have template bloat: there are still 8 functions in 
the executable. Then consider
void evenworse(x, y)
where both x AND y need special treatment if they are ulong. 8*8 = 64 
functions go into the executable, but only 4 of them are different! Ouch.

If only there was a way to disable implicit conversions...

void add()(long x)
{
     assert(x == 7);
}

void add(Tulong)(Tulong x)   if ( is(Tulong == ulong) )
{
     assert(x == 6);
}

void main()
{
     add(7L);
     add(6UL);
     add(7); // Look ma, no conflicts! No bloat!
}

Template constraints are seriously cool.
Mar 19 2010
next sibling parent reply bearophile <bearophileHUGS lycos.com> writes:
Don:

Do you know why this semantics:

 void add(Tulong)(Tulong x)   if ( is(Tulong == ulong) )
Is different from this one? void add(Tulong:ulong)(Tulong x) { This page says: http://www.digitalmars.com/d/2.0/templates-revisited.html T:int, // T must be int type So it seems the same as the version with the template constraint. Can't the D2 semantics of (Tulong:ulong) be redefined to become the same of the version with template constraint if(is(Tulong==ulong)) to remove this special case from D?
 Template constraints are seriously cool.
I agree :-) And Andrei has used them a lot in Phobos2. Thank you for your trick (but but the idea is to have a language that needs as few tricks as possible). Bye, bearophile
Mar 19 2010
parent reply Don <nospam nospam.com> writes:
bearophile wrote:
 Don:
 
 Do you know why this semantics:
 
 void add(Tulong)(Tulong x)   if ( is(Tulong == ulong) )
Is different from this one? void add(Tulong:ulong)(Tulong x) { This page says: http://www.digitalmars.com/d/2.0/templates-revisited.html T:int, // T must be int type So it seems the same as the version with the template constraint.
It isn't. It's the same as: add(ulong x) which leads to an ambiguity with add(long x). Template constraints are considerably more powerful than template specialisation.
 
 Can't the D2 semantics of (Tulong:ulong) be redefined to become the same of
the version with template constraint if(is(Tulong==ulong)) to remove this
special case from D?
Not without loss of capability.
Mar 19 2010
parent bearophile <bearophileHUGS lycos.com> writes:
Don:
 Not without loss of capability.
So this means "all separately instantiated add() functions with T that the compiler can automatically cast to uint": void add(T:ulong)(T x) { So it's something very different from the template constraint, that just accepts/refuses to see the template according to the if clause. I have programmed D1 for years thinking add(T:ulong) means something different, a single instantiation with ulong. I was wrong. Thank you, as usual, bearophile
Mar 19 2010
prev sibling parent reply Justin Johansson <no spam.com> writes:
The problem is that D does not have any formal semantics for its
informal type system (this, of course, being a tautological statement).

The discovery of any "cool idiom" for disabling "pesky implicit
conversion" is tantamount to engaging the same sorts of hacks
that web programmers do with CSS (Cascading Style Sheets). Need
I elaborate?

People might scoff as they surely will but to even begin to
understand formalism in data types this would be a good
place to start ...

W3C XML Schema Definition Language (XSD) 1.1 Part 2: Datatypes

http://www.w3.org/TR/2009/WD-xmlschema11-2-20091203/

Editors (Version 1.1):
    David Peterson, invited expert (SGMLWorks!) <davep iit.edu>

    Ashok Malhotra, Oracle Corporation <ashokmalhotra alum.mit.edu>
    C. M. Sperberg-McQueen, Black Mesa Technologies LLC
<cmsmcq blackmesatech.com>
    Henry S. Thompson, University of Edinburgh <ht inf.ed.ac.uk>

Regards
Justin Johansson


Don Wrote:

 I've found a pretty cool idiom for disabling pesky implicit conversion.
 
 This is a situation that I've encountered with BigInt; but I think it's 
 a general problem.
 I have an "add integer" operation. Let's call it:
 void add( x ), where x is a built-in integral type.
 
 If I define add(long x), everything works great -- it can be called with 
 x as a byte, ubyte, short, ushort, int, uint, or long.
 Well, almost. Unfortunately, it doesn't work for ulong: add(ulong.max) 
 will either fail to compile, or get implicitly cast to add(-1)!!
 
 You can fix this by defining add(ulong x) as a special case. Now x can 
 be long or ulong, and it works. Great!
 Problem is, now if x is an integer, the compiler complains that it's 
 ambiguous -- should it call the long or the ulong version? Bummer!
 
 You could solve that by creating int, uint, short, ushort,... functions. 
 Eight in total, but seven of them are identical. Massive source code 
 duplication. So use a template:
 
 void add(T)(T x)
 {
    // everything except ulong
 }
 
 void add(T : ulong)(T x)
 {
 }
 
 Problem solved! Well, not quite. Now the source code duplication is 
 gone, but you still have template bloat: there are still 8 functions in 
 the executable. Then consider
 void evenworse(x, y)
 where both x AND y need special treatment if they are ulong. 8*8 = 64 
 functions go into the executable, but only 4 of them are different! Ouch.
 
 If only there was a way to disable implicit conversions...
 
 void add()(long x)
 {
      assert(x == 7);
 }
 
 void add(Tulong)(Tulong x)   if ( is(Tulong == ulong) )
 {
      assert(x == 6);
 }
 
 void main()
 {
      add(7L);
      add(6UL);
      add(7); // Look ma, no conflicts! No bloat!
 }
 
 Template constraints are seriously cool.
Mar 19 2010
parent reply "Uranuz" <neuranuz gmail.com> writes:
I revived this topic, because I have similar problem but with 
additional requirements.

I have interface and some classes that inherits from it. Also I 
have property two overloads of *set* properties with types int 
and Nullable!int

There is listing of code that illustrates funny bug that I had in 
my project.

import std.stdio, std.typecons;


interface ListControl
{
	 property {
		void selValue(int value);
	
		void selValue(Nullable!int value);
	}
}


class CheckBoxList: ListControl
{
	override  property {
		void selValue(int value)
		{
			writeln("value: int");
		}
	
		void selValue(Nullable!int value)
		{
			writeln("value: Nullable!int");
		}
	}
	
}


void main()
{
	//Someone considered to make it of byte type
	//instead of int to save memory
	Nullable!byte myValue; //it is null/uninitialized
	
	//Creating object
	auto checkBoxList = new CheckBoxList;
	
	//Let's assign value to our property
	//You see that types don't match. But let's imagine that it is a 
complicated project
	//and class implementation and *myValue* located in different 
modules so programmer don't remember right type
	checkBoxList.selValue = myValue; //This just silently fails in 
runtime without some compile-time error
}

I spend some time to localize it. I know the source of problem, 
so if you haven't guessed it I'll explain.

std.typecons.Nullable uses *alias this* to implicitly access 
payload of Nullable. I like it, because it makes code shorter and 
allows to work with Nullable like it is underlying value (I'm not 
sure about how this sentence sounds in English). But... but... 
When using it as property it provides TOO MUCH implicit castings 
for me where I not expect it be.

So in the listing assignment doesn't call Optional!int overload 
of property (types don't match), but instead it implicitly calls 
get of Nullable. It's return value is byte. Byte is implicitly 
convertible to int. The only problem that myValue has *Null* 
state and get fails with assertion.

It is not what programmer wants to see as a result. The worst 
case he expects is compile-time error about type mismatch.

But it is not all! In this topic solution to it is to use 
cure-all template methods that will help to know *real* type of 
*myValue*. But... but... As you see in the example above I want 
to have *virtual* methods, that can't be of template nature in D. 
So this solution doesn't work there.

Is there in D the other way to say that I want only 
direct/explicit (I don't know how express it correctly in 
English) type match in virtual property method without using 
templates? Is there some *explicit* keyword (I'm joking).

C++11 has explicit keyword but it's purpose is different from 
what I'm saying.

It's interesting to see any ideas, because I have doubts of using 
Nullable and *alias this* feature because of bugs like this.
Aug 26 2014
parent reply "Uranuz" <neuranuz gmail.com> writes:
OK. Nobody ansvered anything yet. So I was thinking about this 
problem for a while so I figured out possible soulution.

What I can do in this situation in to forbid these opertions and 
make compiler issue an error during compilation or implement 
functionality for all types that implicitly convertible to int.

But what I don't like about this idea is if I forget about some 
type or something will change in the future it can become failing 
in runtime but will issue an error compile-time. Also I need to 
implement a lot of methods.

In my real project ListControl is interface template so 
CheckListBox is also class template.

import std.stdio, std.typecons, std.traits, std.typetuple;


interface ListControl(ValueType)
{
	 property {
		void selValue(ValueType value);
	
		void selValue(Nullable!ValueType value);
		
		static if( is( ValueType == int ) )
		{
			mixin(genSetSelValueDecls(IntCastedTypes));
		}
		else static if( is( ValueType == uint) )
		{
			//Declaration for uint type
		}
	}
}

enum IntCastedTypes = ["bool", "byte", "ubyte", "short", 
"ushort", "char", "wchar"];

string genSetSelValueDecls(string[] types)
{
	string result;
	
	foreach( type; types )
		result ~= `
			void selValue(Nullable!` ~ type ~ ` value);
		`;
	
	return result;
}

string genSetSelValueMethods(string[] types)
{
	string result;
	
	foreach( type; types )
		result ~= `
			void selValue(Nullable!` ~ type ~ ` value)
			{	writeln("value: ", typeof(value).stringof);
			}
		`;
	
	return result;
}


class CheckBoxList(ValueType): ListControl!(ValueType)
{
	override  property {
		void selValue(ValueType value)
		{
			writeln("value: ", typeof(value).stringof);	
		}
	
		void selValue(Nullable!ValueType value)
		{
			writeln("value: ", typeof(value).stringof);	
		}
		
		static if( is( ValueType == int ) )
		{
			mixin(genSetSelValueMethods(IntCastedTypes));
		}
		else static if( is( ValueType == uint) )
		{
			//implementation for uint type
		}
	}

}

void main()
{
	Nullable!ubyte myValue; //it is null/uninitialized
	
	//Creating object
	auto checkBoxList = new CheckBoxList!int;
	
	checkBoxList.selValue = myValue;
}

So this is working and does what is expected to do. Although is 
should be unittested pretty well to eliminate all sorts of bugs.

Types that implicitly convertible to int were taken from:
http://dlang.org/type.html (IntegerPromotions)

But what I noticed is that uint type is implicitly convertible to 
int to. So this is not covered by example because it is not 
documented case. May be it is just nasty bug in compiler (I don't 
know but I think it is).

I thing this is a bug because uint.max cannot be correctly 
represented in int and shouldn't be implicitly convertibe. I 
raised problems with conversions between integer types a time ago 
but it had no result or resolution. And it's strange for me that 
so trivial aspects of language become source of nasty bugs.
For example:

import std.stdio;

void main()
{
	uint a = uint.max;
	writeln(a); // 4294967295
	
	int b = a;
	writeln(b); // -1
}

I don't like the situation that when implementing new feature in 
my library I must fight the compiler in order to get what 
obviously expected from it. I like the language but lots of small 
bugs makes me get upset.

Please fix integers! I want to use all of them: ubyte, long, 
short. Not only int that is considerer as priorite for some 
reason that I don't understand. I don't thing that compatibility 
with C/C++ reasons should create such a mess about integer type 
casts. I haven't such problems whaen I was programming in C/C++. 
Also notice that C is language with weak type system but D is 
declared to have type system. So rules should be slightly 
different.

Of course all of the above is just my own opinion and language 
designers may do whatever they like. But...
Aug 26 2014
parent reply "Uranuz" <neuranuz gmail.com> writes:
In the pre-last paragraph please read text^
Also notice that C is language with weak type system but D is
declared to have type system.

as:
Also notice that C is language with weak type system but D is
declared to have *strong* type system.
Aug 26 2014
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 08/26/2014 05:46 PM, Uranuz wrote:
 In the pre-last paragraph please read text^
 Also notice that C is language with weak type system but D is
 declared to have type system.

 as:
 Also notice that C is language with weak type system but D is
 declared to have *strong* type system.
It's not _that_ strong. Also, this is a library problem. I'd suggest you file a bug report/enhancement request against Phobos, if it is not already there. https://issues.dlang.org/ It seems that adding some constructor(s)/opAssign(s) to Nullable will solve this problem. ( BTW: You are more likely to get a quick response if your post looks a little bit more digestible, like: On 08/26/2014 05:38 PM, Uranuz could have written:
 // ---
 import std.typecons;
 void main(){
     Nullable!ubyte x;
     Nullable!int y=x; // AssertError: Called 'get' on null Nullable!ubyte
 }
 // ---
 import std.typecons;
 void main(){
     Nullable!ubyte x;
     Nullable!int y;
     y=x; // AssertError: Called 'get' on null Nullable!ubyte
 }
 // ---

 Is this a bug?
Or you could have gone straight to https://issues.dlang.org/. :) )
Aug 26 2014
parent reply "Uranuz" <neuranuz gmail.com> writes:
On Tuesday, 26 August 2014 at 16:48:08 UTC, Timon Gehr wrote:
 On 08/26/2014 05:46 PM, Uranuz wrote:
 In the pre-last paragraph please read text^
 Also notice that C is language with weak type system but D is
 declared to have type system.

 as:
 Also notice that C is language with weak type system but D is
 declared to have *strong* type system.
It's not _that_ strong. Also, this is a library problem. I'd suggest you file a bug report/enhancement request against Phobos, if it is not already there. https://issues.dlang.org/ It seems that adding some constructor(s)/opAssign(s) to Nullable will solve this problem. ( BTW: You are more likely to get a quick response if your post looks a little bit more digestible, like: On 08/26/2014 05:38 PM, Uranuz could have written:
 // ---
 import std.typecons;
 void main(){
    Nullable!ubyte x;
    Nullable!int y=x; // AssertError: Called 'get' on null 
 Nullable!ubyte
 }
 // ---
 import std.typecons;
 void main(){
    Nullable!ubyte x;
    Nullable!int y;
    y=x; // AssertError: Called 'get' on null Nullable!ubyte
 }
 // ---

 Is this a bug?
Or you could have gone straight to https://issues.dlang.org/. :) )
The last example is even more obvious but didn't come to my mind. Maybe I'l think of better implementation myself and push something to standard library. The best way to make something working is to do it myself [jokingly]
Aug 26 2014
parent "Uranuz" <neuranuz gmail.com> writes:
On Tuesday, 26 August 2014 at 18:29:49 UTC, Uranuz wrote:
 On Tuesday, 26 August 2014 at 16:48:08 UTC, Timon Gehr wrote:
 On 08/26/2014 05:46 PM, Uranuz wrote:
 In the pre-last paragraph please read text^
 Also notice that C is language with weak type system but D is
 declared to have type system.

 as:
 Also notice that C is language with weak type system but D is
 declared to have *strong* type system.
It's not _that_ strong. Also, this is a library problem. I'd suggest you file a bug report/enhancement request against Phobos, if it is not already there. https://issues.dlang.org/ It seems that adding some constructor(s)/opAssign(s) to Nullable will solve this problem. ( BTW: You are more likely to get a quick response if your post looks a little bit more digestible, like: On 08/26/2014 05:38 PM, Uranuz could have written:
 // ---
 import std.typecons;
 void main(){
   Nullable!ubyte x;
   Nullable!int y=x; // AssertError: Called 'get' on null 
 Nullable!ubyte
 }
 // ---
 import std.typecons;
 void main(){
   Nullable!ubyte x;
   Nullable!int y;
   y=x; // AssertError: Called 'get' on null Nullable!ubyte
 }
 // ---

 Is this a bug?
Or you could have gone straight to https://issues.dlang.org/. :) )
The last example is even more obvious but didn't come to my mind. Maybe I'l think of better implementation myself and push something to standard library. The best way to make something working is to do it myself [jokingly]
If I could write multiple alias this in Nullable I could create implicit conversions from Nullable!byte not only to byte but also to Nullable!short, Nullable!int. In this case there is no need to implement lots of overloads for property of type Nullable!int in order to correctly accept Nullable!byte, Nullable!ubyte and so on. I have wrote an example code and seems that only fixing Nullable implementation will not help to eliminate the problem I was talking about. So I have a choice: 1. Implement Nullable type without *alias this* at all (and access to internal value with getter method or property) 2. Implement lots of overloads of virtual Nullable!int property to accept every minor integer types. Also is needed to forbid Nullable!uint conversion because it is incorrect. I can't correctly store uint.max inside int variable, but implicit coversion is allowed in the compiler (and it is not documented). Because using alias this makes code shorter I prefer it, although it is very tricky feature and needs attention during development.
Aug 27 2014