www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - CommonType and non-built-in types

reply Joseph Rushton Wakeling <joseph.wakeling webdrake.net> writes:
Hello all,

In the course of examining std.rational I've had to take a look inside 
std.traits.CommonType, and I'm hoping people can help me to understand some
fine 
details which I'm currently unsure of.

The essence of the CommonType template is simple:

     * If it is passed no arguments, the common type is void.

     * If it is passed one argument, the common type is the type of that
       argument.

     * If it is passed more than one argument, it looks for the common type U
       between the first 2 arguments.  If it finds it, then it returns the
       common type of U and the remaining arguments (in other words, it
       recursively identifies the common types of successive arguments until
       none are left).

     * If the first 2 arguments can't be implicitly converted, it returns void.

A consequence of this is that CommonType will not necessarily work nicely with 
many non-built-in types.  For example, the common type of BigInt and int is 
void, even though in principle it should be possible to convert an int to a 
BigInt.  It's this that is particularly of concern to me.

Anyway, to concrete questions.

(1) Can someone please explain to me _in detail_ the mechanics of the code
which 
identifies whether the first 2 template arguments have a common type?

I understand what it does, but not why/how it does it, if you get me :-)

     static if (is(typeof(true ? T[0].init : T[1].init) U))
     {
         alias CommonType!(U, T[2 .. $]) CommonType;
     }

(2) Same code -- why is it only necessary to check T[0].init : T[1].init and
not 
vice versa?  (Yes, you can tell I don't really understand the : operator
properly:-)

(3) What would one have to implement in a library-defined type to enable 
T[0].init : T[1].init to evaluate to true?  For example, to enable int and 
BigInt to be compatible?

(4) Is there a good reason why there _shouldn't_ be a CommonType of (say) int 
and BigInt?

I'm sure I'll think up more questions, but this seems enough to be going on
with 
... :-)

Thanks & best wishes,

     -- Joe
Oct 01 2013
next sibling parent reply "John Colvin" <john.loughran.colvin gmail.com> writes:
On Tuesday, 1 October 2013 at 10:50:39 UTC, Joseph Rushton 
Wakeling wrote:
 (1) Can someone please explain to me _in detail_ the mechanics 
 of the code which identifies whether the first 2 template 
 arguments have a common type?

 I understand what it does, but not why/how it does it, if you 
 get me :-)

     static if (is(typeof(true ? T[0].init : T[1].init) U))
     {
         alias CommonType!(U, T[2 .. $]) CommonType;
     }

 (2) Same code -- why is it only necessary to check T[0].init : 
 T[1].init and not vice versa?  (Yes, you can tell I don't 
 really understand the : operator properly:-)
This contains quite a bit of trickery. A ternary expression must evaluate to a single, statically known type. Therefore, true ? T[0].init : T[1].init will only be a valid expression if there is a common type between T[0] and T[1] From the spec dlang.org/expression.html:
 Conditional Expressions
 
 ConditionalExpression:
     OrOrExpression
     OrOrExpression ? Expression : ConditionalExpression
 
 The first expression is converted to bool, and is
 evaluated. If it is true, then the second expression
 is evaluated, and its result is the result of the conditional 
 expression. If it is false, then the third
 expression is evaluated, and its result is the result of the 
 conditional expression. If either the second or third 
 expressions are of type void, then the resulting type is void. 
 Otherwise, the second and third expressions are implicitly 
 converted to a common type which becomes
 the result type of the conditional expression.
If there is a common type then U is declared as an alias of it and the is expression returns true. If there isn't a common type, the ternary expression is an error and the is expression will return false. In the first case we just recursively traverse the type list, in the second we declare the common type to be void. Basically, it's just a wrapper around some compiler magic. All the common type calculation is all done by the compiler as a convenient effect of the ternary operator.
Oct 01 2013
parent Joseph Rushton Wakeling <joseph.wakeling webdrake.net> writes:
On 01/10/13 13:13, John Colvin wrote:
 This contains quite a bit of trickery. A ternary expression must evaluate to a
 single, statically known type. Therefore, true ? T[0].init : T[1].init will
only
 be a valid expression if there is a common type between T[0] and T[1]
Ohh! <light dawns>It's saying "U is the type of the expression (true? T[0] : T1) and if is(U), i.e. if U exists, then ..." And U will only exist if as you say there is a common type, which is inferred here.</light dawns> I'd misinterpreted the meaning of T[0] : T[1] completely. (More on that in my reply to monarchdodra.) Thanks very much!
Oct 02 2013
prev sibling next sibling parent reply "Dicebot" <public dicebot.lv> writes:
On Tuesday, 1 October 2013 at 10:50:39 UTC, Joseph Rushton 
Wakeling wrote:
 (1) Can someone please explain to me _in detail_ the mechanics 
 of the code which identifies whether the first 2 template 
 arguments have a common type?

 I understand what it does, but not why/how it does it, if you 
 get me :-)

     static if (is(typeof(true ? T[0].init : T[1].init) U))
     {
         alias CommonType!(U, T[2 .. $]) CommonType;
     }
a) Ternary operator returns value of type that can be used to store values of both condition branches. Don't know exact algorithm in compiler do determine this but it is likely based on presence of implicit conversion. b) If it is impossible to use both types in ternary operator, no implicit conversions (and thus no common type) are possible. Thus it will result in compilation error and typeof() of whole expression will be invalid type, making is() return false. c) If it is a valid expression, its type is CommonType and aliased to U. Then template is recursively called to find common type between U and rest of template parameter list. d) In the end who;e CommonType template will be aliased to last such U in recursion chain.
 (2) Same code -- why is it only necessary to check T[0].init : 
 T[1].init and not vice versa?  (Yes, you can tell I don't 
 really understand the : operator properly:-)
It is not is(T : U) form but ternary operator.
 (3) What would one have to implement in a library-defined type 
 to enable T[0].init : T[1].init to evaluate to true?  For 
 example, to enable int and BigInt to be compatible?
It requires int to be implicitly convertible to BigInt. Don't know if we have tools to do it.
 (4) Is there a good reason why there _shouldn't_ be a 
 CommonType of (say) int and BigInt?
If it is possible to express it, common type of those should be BigInt.
Oct 01 2013
parent "Dicebot" <public dicebot.lv> writes:
P.S. adding implicit conversion other way around is possible via 
`alias this` but I don't think it is a valid behavior. (as common 
type of (`long`, `int`, `short`) is `long`)
Oct 01 2013
prev sibling parent reply "monarch_dodra" <monarchdodra gmail.com> writes:
On Tuesday, 1 October 2013 at 10:50:39 UTC, Joseph Rushton 
Wakeling wrote:
 Hello all,

 In the course of examining std.rational I've had to take a look 
 inside std.traits.CommonType, and I'm hoping people can help me 
 to understand some fine details which I'm currently unsure of.

 The essence of the CommonType template is simple:

     * If it is passed no arguments, the common type is void.

     * If it is passed one argument, the common type is the type 
 of that
       argument.

     * If it is passed more than one argument, it looks for the 
 common type U
       between the first 2 arguments.  If it finds it, then it 
 returns the
       common type of U and the remaining arguments (in other 
 words, it
       recursively identifies the common types of successive 
 arguments until
       none are left).

     * If the first 2 arguments can't be implicitly converted, 
 it returns void.

 A consequence of this is that CommonType will not necessarily 
 work nicely with many non-built-in types.  For example, the 
 common type of BigInt and int is void, even though in principle 
 it should be possible to convert an int to a BigInt.  It's this 
 that is particularly of concern to me.

 Anyway, to concrete questions.

 (1) Can someone please explain to me _in detail_ the mechanics 
 of the code which identifies whether the first 2 template 
 arguments have a common type?

 I understand what it does, but not why/how it does it, if you 
 get me :-)

     static if (is(typeof(true ? T[0].init : T[1].init) U))
     {
         alias CommonType!(U, T[2 .. $]) CommonType;
     }

 (2) Same code -- why is it only necessary to check T[0].init : 
 T[1].init and not vice versa?  (Yes, you can tell I don't 
 really understand the : operator properly:-)

 (3) What would one have to implement in a library-defined type 
 to enable T[0].init : T[1].init to evaluate to true?  For 
 example, to enable int and BigInt to be compatible?

 (4) Is there a good reason why there _shouldn't_ be a 
 CommonType of (say) int and BigInt?

 I'm sure I'll think up more questions, but this seems enough to 
 be going on with ... :-)

 Thanks & best wishes,

     -- Joe
The code basically uses operator ?: which is basically: auto oeprator(T1, T2)(bool cont, T1 lhs, T2 rhs) { if (cond) return lhs; else return rhs; } By using the operator's return type, you get, basically, what the compiler believes is the "common type" that you'd get from either a T1, or a T2. Back to the code: static if (is(typeof(true ? T[0].init : T[1].init) U)) This basically checks if ternary compiles, and if it does, "assigns" the return type to U, after which, the common type becomes U.
 (2) Same code -- why is it only necessary to check T[0].init : 
 T[1].init and not vice versa?  (Yes, you can tell I don't 
 really understand the : operator properly:-)
Order makes no difference.
 (3) What would one have to implement in a library-defined type 
 to enable T[0].init : T[1].init to evaluate to true?
I think you are reading the code wrong, it's not "T[0].init : T[1].init" that evaluates to "true". It's the argument of the ternary operator. "true" is just a dummy placeholder. What this code is checking is that "condition ? T[0].init : T[1].init" compiles at all.
 (4) Is there a good reason why there _shouldn't_ be a 
 CommonType of (say) int and BigInt?
Well, given that D doesn't allow implicit construction, and that the entire point of "CommonType" (AFAIK) is to check the *implicit* common type, it would be a little difficult.
Oct 01 2013
parent Joseph Rushton Wakeling <joseph.wakeling webdrake.net> writes:
On 01/10/13 13:15, monarch_dodra wrote:
 By using the operator's return type, you get, basically, what the compiler
 believes is the "common type" that you'd get from either a T1, or a T2.

 Back to the code:
 static if (is(typeof(true ? T[0].init : T[1].init) U))

 This basically checks if ternary compiles, and if it does, "assigns" the return
 type to U, after which, the common type becomes U.

 (2) Same code -- why is it only necessary to check T[0].init : T[1].init and
 not vice versa?  (Yes, you can tell I don't really understand the : operator
 properly:-)
Order makes no difference.
 (3) What would one have to implement in a library-defined type to enable
 T[0].init : T[1].init to evaluate to true?
I think you are reading the code wrong, it's not "T[0].init : T[1].init" that evaluates to "true". It's the argument of the ternary operator. "true" is just a dummy placeholder. What this code is checking is that "condition ? T[0].init : T[1].init" compiles at all.
Yea, I think I had in my head "This is generic/static/compile time/template stuff, and the only time I've ever seen A : B with respect to types is when you're attempting to indicate that A is implicitly convertible to B." I had a "D'oh!" moment when I realized shortly after sending the email that it was actually probably a condition ? ifTrue : ifFalse style expression, but still didn't get what that meant here or why it did what I did. Now that you and others have explained this, I'm impressed. It's a very nice example of how template code in D really is just like "real" code except that the variables are template parameters rather than variables.
 Well, given that D doesn't allow implicit construction, and that the entire
 point of "CommonType" (AFAIK) is to check the *implicit* common type, it would
 be a little difficult.
Ahh. OK, this makes things clear, and explain why David Simcha needed things like CommonRational and CommonInteger in his std.rational.
Oct 02 2013