Thread overview
template constraints when non-constrained template exists
Oct 05, 2015
Manu
Oct 05, 2015
Dicebot
Oct 05, 2015
Jonathan M Davis
Oct 05, 2015
Manu
Oct 05, 2015
Jonathan M Davis
Oct 05, 2015
Manu
Oct 05, 2015
Jonathan M Davis
October 05, 2015
So, this might be a really nub question, but it's a problem that constantly comes up, I tend to find some simple workaround, and it goes away... but I need to understand.

template X(T)   // this template is EVERYWHERE in phobos, look in std.traits
{
  static if(isOkay(T))
  {
    ...
  }
  else
    static assert(0, "T is no good!");
}

Given this, the trouble is X(T) accepts anything, and it wasn't made to handle a particular T, so I get the static assert.

So I want to add alongside:

template X(T) if(someCondition!T)
{
 ...
}

The idea being to handle T's that the original template fails at...
but this just doesn't seem to work.
I just get fir instance: "Error: template std.traits.Signed matches
more than one template declaration: ..."

Which is true, but shouldn't it pick the most appropriate match? In this case, the one that matches the given constraint I would think is a better match than the open catch-all version.

My specific problem is that I want Signed!, Unsigned!, isSigned!, usUnsigned!, etc to work with my type. Looking at Unsigned!T for instance, it tests UnsignedTypeOf!T, and that tests IntegralTypeOf!T

Ah-hah, surely I just need to declare IntegralTypeOf!T for my type, and it all comes good? But nope, the problem above.

This comes up in many cases, I presume I've just missed something really obvious...?
October 05, 2015
AFAIK D has intentionally simplified resolution rules to make it possible to reason about what is best match without referring to copy of ANSI standard :) And plain unconstrained `T` is thus as good match as any other.

In your example intended approach is to either put `if(someCondition!T)` into original template as another `static if` branch or turn first template into constrained one too.
October 05, 2015
On Monday, 5 October 2015 at 08:14:14 UTC, Manu wrote:
> This comes up in many cases, I presume I've just missed something really obvious...?

The obvious thing is to change it so that the template declaration in Phobos actually has a template constraint on it. But in general, these sorts of templates were not designed with the idea that they would be overloaded in user code. And to be honest, in this case, it could be a big problem if they were. These traits are used in code under the assumption that they only allow built-in types, and if you could overload them, then suddenly, that types would get past the template constraints on templates which were not designed to handle anything but built-in types. And at least some of the documentation is clear that they are intended for built-in types only. e.g. this is the ddoc comment on isUnsigned:

    Detect whether $(D T) is a built-in unsigned numeric type.

So, you could certainly declare your own isUnsigned and similar traits, but they're not going to overload with the ones in std.traits and thus would likely have to fully qualified. I suspect that that thwarts what you're trying to do, but I think that it's pretty clear that these traits were _not_ intended to be true for any user-defined type.

In general, the traits in std.traits are designed to match an exact set of built-in types and that's it. And code using them is going to rely on that, making overloading them very risky IMHO.

- Jonathan M Davis
October 05, 2015
On 5 October 2015 at 18:26, Jonathan M Davis via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
> On Monday, 5 October 2015 at 08:14:14 UTC, Manu wrote:
>>
>> This comes up in many cases, I presume I've just missed something really obvious...?

> So, you could certainly declare your own isUnsigned and similar traits, but they're not going to overload with the ones in std.traits and thus would likely have to fully qualified. I suspect that that thwarts what you're trying to do, but I think that it's pretty clear that these traits were _not_ intended to be true for any user-defined type.

Right, exactly. In most cases where this has come up for me, I'm
wrapping a built-in type in a thin skin to implement some specific
semantics.
In one case, I wanted a safer range limited integer with invariants to
prove the ranges. In my case today, I'm implementing a saturating
integer type (in the case of overflow it will clamp to min/max). These
are intended for injection into contexts where built-in's are
typically used, and they require interoperation with the standard
traits, otherwise I have to wrap all the standard traits needlessly
for every new type of this sort.

I've worked around this on many occasions, but it's always a bit nasty, and in this case, it's particularly nasty because I'm interacting with code I don't control.

Consider, if I were to submit a saturating integer to phobos, and then submit PR's for support of the saturating int to std.traits, I'm almost certain it would be rejected on account of adding unrelated things to std.traits (if this, then why not the range limited int? Why not fixed point? Why not Walter's half-float? These should all respond appropriately to std.traits.

I think this problem needs a hygienic solution one way or another.


> In general, the traits in std.traits are designed to match an exact set of built-in types and that's it. And code using them is going to rely on that, making overloading them very risky IMHO.

I don't think it's risky, I think it should be expected. Surely a type which has a concept of 'isSigned' should respond accordingly to the standard introspection tools? It would be the responsibility of the author of a fully-featured library to make sure this works.
October 05, 2015
On Monday, 5 October 2015 at 10:17:06 UTC, Manu wrote:
> On 5 October 2015 at 18:26, Jonathan M Davis via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>> In general, the traits in std.traits are designed to match an exact set of built-in types and that's it. And code using them is going to rely on that, making overloading them very risky IMHO.
>
> I don't think it's risky, I think it should be expected. Surely a type which has a concept of 'isSigned' should respond accordingly to the standard introspection tools? It would be the responsibility of the author of a fully-featured library to make sure this works.

These traits were written and documented as only working with built-in types. Changing that now could easily mean that existing code would would then allow user-defined traits passed template constraints on templates that will not work with user-defined types - especially when you consider that these traits could be combine with other traits to restrict what's accepted to a fairly specific subset.

If these traits had be designed from the get-go with the idea that they would work with any user-defined type that met certain criteria, then that would be different, but they weren't.

Also, I think that a trait which _was_ supposed to work with user-defined types would have to be very carefully written. This very quickly gets into the camp where implicit conversions sit, and those can be _really_ bad for template constraints, because it's very easy to write a template constraint that accepts implicit conversions and then end up with a template that works with the target type but does not actually work with the types that implicitly convert to it. They have their place, but you have to be very careful with them.

It probably does make sense to declare some traits designed to work with both the built-in types and user-defined types for some of these arithmetic operations, but I expect that they would have to be written very carefully to avoid problems, and I don't think that it makes sense to change the existing traits to work that way. I would be very surprised if such a change did not break existing code.

- Jonathan M Davis
October 05, 2015
On 5 October 2015 at 20:30, Jonathan M Davis via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
> On Monday, 5 October 2015 at 10:17:06 UTC, Manu wrote:
>>
>> On 5 October 2015 at 18:26, Jonathan M Davis via Digitalmars-d <digitalmars-d@puremagic.com> wrote:
>>>
>>> In general, the traits in std.traits are designed to match an exact set of built-in types and that's it. And code using them is going to rely on that, making overloading them very risky IMHO.
>>
>>
>> I don't think it's risky, I think it should be expected. Surely a type which has a concept of 'isSigned' should respond accordingly to the standard introspection tools? It would be the responsibility of the author of a fully-featured library to make sure this works.
>
>
> These traits were written and documented as only working with built-in types. Changing that now could easily mean that existing code would would then allow user-defined traits passed template constraints on templates that will not work with user-defined types - especially when you consider that these traits could be combine with other traits to restrict what's accepted to a fairly specific subset.
>
> If these traits had be designed from the get-go with the idea that they would work with any user-defined type that met certain criteria, then that would be different, but they weren't.

So... they may be subject to new bug reports when the constraints are lifted?

> Also, I think that a trait which _was_ supposed to work with user-defined types would have to be very carefully written. This very quickly gets into the camp where implicit conversions sit, and those can be _really_ bad for template constraints, because it's very easy to write a template constraint that accepts implicit conversions and then end up with a template that works with the target type but does not actually work with the types that implicitly convert to it. They have their place, but you have to be very careful with them.

Extending a trait would only happen in the context that the module
that extends it is also made present within the same scope; ie,
`import std.traits, saturatingint;`
With that in mind, it's pretty unlikely you'd have weird hijackings
and incompatibilities appearing all over the place since the import
statement pretty much makes your intent to use a thing clear.

> It probably does make sense to declare some traits designed to work with both the built-in types and user-defined types for some of these arithmetic operations, but I expect that they would have to be written very carefully to avoid problems, and I don't think that it makes sense to change the existing traits to work that way. I would be very surprised if such a change did not break existing code.
>
> - Jonathan M Davis

I am personally yet to give a single f... care, when any change that
makes D better also breaks code. And that includes the time when I was
maintaining a reasonably substantial codebase used commercially.
D is not finished yet, not by a long shot. I'm so bored of this
excuse, it's predictable and seriously tiring. (not from you
specifically, it comes from all angles)

But anyway, what do I do? I need to inject one of these types into a
tree which uses std.traits.
This demonstrates a fairly serious scalability problem. It's come up
quite some number of times for me.

D is all about composition, D is all about duck typing. I'm doing
exactly the sort of thing that modern idiomatic D encourages you to
do.
This isn't an acceptable end-of-the-story, but I'll leave it here, and
other people can decide what's right.
October 05, 2015
On Monday, 5 October 2015 at 11:05:51 UTC, Manu wrote:
> I am personally yet to give a single f... care, when any change that
> makes D better also breaks code. And that includes the time when I was
> maintaining a reasonably substantial codebase used commercially.
> D is not finished yet, not by a long shot. I'm so bored of this
> excuse, it's predictable and seriously tiring. (not from you
> specifically, it comes from all angles)

In this case, I really think that we need the traits as they are now regardless of whether changing them would break anything, because I think that we need traits like this which work only with built-in types. Without that, plenty of code is going to have to use them with one or more additional checks to prevent user-defined types from matching a template constraint. Having additional traits that work with both built-in types and user-defined types would likely be a good idea as well, but that doesn't make sense for all templates.

> But anyway, what do I do? I need to inject one of these types into a
> tree which uses std.traits.
> This demonstrates a fairly serious scalability problem. It's come up
> quite some number of times for me.

Well, feel free to create PRs which add the functionality that you need to Phobos. No matter how good the programmer was who wrote something in Phobos, they're not omniscient. They may very well have reasons why what you want to do is a bad idea or why it should be a separate construct rather than having an existing construct changed based on what you want, or it could be that they simply didn't think of your use case. Heck, it could be that they were aware of it but failed to test for it and wrote the code in way that it accidentally didn't work with what you wanted when it was supposed to.

In this particular case, I think that we should create new traits that test for what you're looking for (rather than altering the existing ones), and possibly, some of the rest of Phobos should be changed to use those, but you can certainly create a PR that solves your problem. And if no one else has the same problem you do, or if all of the people that do assume that someone else will take care of it, then Phobos will never get the improvements that you need even if they are indeed something that the D community as a whole would benefit from and lots of programmers end up complaining about not existing.

The traits are one area where we need to be _really_ careful, since they deal with what will and won't compile with a given template and can interact in a combinatorial explosion of ways, many of them with quite surprising results, but there's no question that they can be improved if only by having more added. We've primarily ended up with traits simply because someone found that they needed one and were able to convince the Phobos devs that it was a general enough need for it to be added, not necessarily because someone planned ahead really well when creating std.traits or even when adding new ones.

- Jonathan M Davis