www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Overloading based on attributes - is it a good idea?

reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.com> writes:
int fun(int) pure;
int fun(int);

pure int gun(int x)
{
    return fun(x);
}

This doesn't work, but on the face of it it's not ambiguous - the second 
overload of fun() would not compile anyway.

I was wondering whether allowing overloading on attributes in general 
would be a good idea. I suspect templates and attribute deduction make 
that difficult.
May 28 2019
next sibling parent Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }

 This doesn't work, but on the face of it it's not ambiguous - 
 the second overload of fun() would not compile anyway.

 I was wondering whether allowing overloading on attributes in 
 general would be a good idea. I suspect templates and attribute 
 deduction make that difficult.
I posed this question a few years ago but specifically for nogc. Maybe it makes sense for some attributes but not for others? I think there could be some good use cases for overloading on nogc, which would allow you to choose a different implementation depending on whether the GC is being used or not.
May 28 2019
prev sibling next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/28/2019 9:08 AM, Andrei Alexandrescu wrote:
 int fun(int) pure;
 int fun(int);
 
 pure int gun(int x)
 {
     return fun(x);
 }
 
 This doesn't work, but on the face of it it's not ambiguous - the second 
 overload of fun() would not compile anyway.
 
 I was wondering whether allowing overloading on attributes in general would be
a 
 good idea. I suspect templates and attribute deduction make that difficult.
More than difficult. I suspect it's impossible in the general case. D does inference of types and attributes from the bottom up, but this would be bottom up and top down. It's easy to know what to do for trivial cases, but for multiple levels and choices, that's graph theory that would be very complex to find a unique solution, and if a unique solution cannot be found, imagine the problems with informing the user just why. It's like overloading based on return value: long fun(int); int fun(int); int gun(int x) { return fun(x); } It's ambiguous even though the first overload of fun() would give an error. Another consideration: class A { int fun() pure; } class B : A { overload int fun(); } Because of covariance/contravariance, B.fun doesn't need to have the pure attribute added, the pureness is inherited from A.fun.
May 28 2019
parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 17:27:08 UTC, Walter Bright wrote:
 On 5/28/2019 9:08 AM, Andrei Alexandrescu wrote:
 int fun(int) pure;
 int fun(int);
 
 pure int gun(int x)
 {
     return fun(x);
 }
 
 This doesn't work, but on the face of it it's not ambiguous - 
 the second overload of fun() would not compile anyway.
 
 I was wondering whether allowing overloading on attributes in 
 general would be a good idea. I suspect templates and 
 attribute deduction make that difficult.
More than difficult. I suspect it's impossible in the general case. D does inference of types and attributes from the bottom up, but this would be bottom up and top down. It's easy to know what to do for trivial cases, but for multiple levels and choices, that's graph theory that would be very complex to find a unique solution, and if a unique solution cannot be found, imagine the problems with informing the user just why. It's like overloading based on return value: long fun(int); int fun(int); int gun(int x) { return fun(x); } It's ambiguous even though the first overload of fun() would give an error. Another consideration: class A { int fun() pure; } class B : A { overload int fun(); } Because of covariance/contravariance, B.fun doesn't need to have the pure attribute added, the pureness is inherited from A.fun.
At first glance this does seem correct. After reading Walter's response here, another thought came to mind that instead of attribute overloading, a template with static-if could also be used. void foo()() { static if (__traits(gcAllowedHere)) { // the GC implementation } else { // the nogc implementation } } void bar1() nogc { foo(); // calls the nogc implementation } void bar2() { foo(); // calls the GC implementation } And the same thing could be applied for purity with something like __traits(inpureAllowedHere). Although, I think we could use existing traits for these, with an "isCompiles" or something.
May 28 2019
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/28/2019 10:35 AM, Jonathan Marler wrote:
 After reading Walter's response here, another thought came to mind that
instead 
 of attribute overloading, a template with static-if could also be used.
nogc attributes are often inferred. Such constructs would make it undecidable.
May 28 2019
next sibling parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 18:07:47 UTC, Walter Bright wrote:
 On 5/28/2019 10:35 AM, Jonathan Marler wrote:
 After reading Walter's response here, another thought came to 
 mind that instead of attribute overloading, a template with 
 static-if could also be used.
nogc attributes are often inferred. Such constructs would make it undecidable.
You're right it's undecidable inside the function, but I think it's decidable if you check it at the call site. So if we could pass that information to the template then we could use it. Using a default template parameter could work, though, we'd likely need to modify __traits(compiles) to work in the callers context when used as a default template parameter, or create a new trait like __trait(compilesAtCallSite): int main(string[] args) { nogcExample(); gcExample(); return 0; } version (WorksToday) { void nogcExample() nogc { allocateString!(__traits(compiles, new Object))(100); } void gcExample() { allocateString!(__traits(compiles, new Object))(100); } auto allocateString(bool allowGC)(size_t size) { static if (allowGC) { return new char[size]; } else { import core.stdc.stdlib : malloc; return (cast(char*)malloc(size))[0 .. size]; } } } else version (UseCompilesAtCallSite) { void nogcExample() nogc { allocateString(100); } void gcExample() { allocateString(100); } auto allocateString(bool allowGC = __traits(compilesAtCallSite, new Object))(size_t size) { static if (allowGC) { return new char[size]; } else { import core.stdc.stdlib : malloc; return (cast(char*)malloc(size))[0 .. size]; } } } Then again maybe if the nogcExample/gcExample functions were also templates, then we wouldn't be able to infer whether or not gc was available...not sure on that one.
May 28 2019
next sibling parent Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 19:49:40 UTC, Jonathan Marler wrote:
 On Tuesday, 28 May 2019 at 18:07:47 UTC, Walter Bright wrote:
 On 5/28/2019 10:35 AM, Jonathan Marler wrote:
 After reading Walter's response here, another thought came to 
 mind that instead of attribute overloading, a template with 
 static-if could also be used.
nogc attributes are often inferred. Such constructs would make it undecidable.
You're right it's undecidable inside the function, but I think it's decidable if you check it at the call site. So if we could pass that information to the template then we could use it. Using a default template parameter could work, though, we'd likely need to modify __traits(compiles) to work in the callers context when used as a default template parameter, or create a new trait like __trait(compilesAtCallSite): int main(string[] args) { nogcExample(); gcExample(); return 0; } version (WorksToday) { void nogcExample() nogc { allocateString!(__traits(compiles, new Object))(100); } void gcExample() { allocateString!(__traits(compiles, new Object))(100); } auto allocateString(bool allowGC)(size_t size) { static if (allowGC) { return new char[size]; } else { import core.stdc.stdlib : malloc; return (cast(char*)malloc(size))[0 .. size]; } } } else version (UseCompilesAtCallSite) { void nogcExample() nogc { allocateString(100); } void gcExample() { allocateString(100); } auto allocateString(bool allowGC = __traits(compilesAtCallSite, new Object))(size_t size) { static if (allowGC) { return new char[size]; } else { import core.stdc.stdlib : malloc; return (cast(char*)malloc(size))[0 .. size]; } } } Then again maybe if the nogcExample/gcExample functions were also templates, then we wouldn't be able to infer whether or not gc was available...not sure on that one.
Another thought. If you wanted to propagate this "allowGC" information, you would probably have to explicitly pass it down each template. For example, if the "nogcExample" and "gcExample" functions were also templates that could optionally use GC, then they would have to explicitly pass that information to the "allocateString template". If that's the case then this doesn't scale. It looks like if you implement this solution fully, the template parameters become analogous to function attributes. So maybe for specific attributes, allowing them to be overloaded might the right way to go. Of course, this brings us back to Andrei's original question which is whether or not allowing attributes to be overloaded would be worth the trouble to support.
May 28 2019
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/28/2019 12:49 PM, Jonathan Marler wrote:
 You're right it's undecidable inside the function, but I think it's decidable
if 
 you check it at the call site.
Yet the call site may also be doing attribute inference. Think of a graph with nodes in it, all interconnected with arbitrary edges, including cycles, and you have to find a set of attributes that satisfy each of the edges in it, when adding any attribute changes the topology of the graph. Even if there is an algorithm which can solve this, and I don't have a PhD in graph theory and have no idea if there is one or not: 1. there may be N solutions - which one is picked? 2. how do you explain to the user why? 3. how do you explain to the user when N is zero? 4. the combinatorics of this may mean it takes essentially infinite time 5. I have a hard enough time implementing/debugging the current bottom-up method 6. I've spent years dealing with problems with forward references, when there are cycles in the reference graph and incomplete types. Your proposal makes that infinitely worse. Like I said, it looks workable for trivial boundary cases, but that isn't how things work when people start using such a feature. So, no. Hell no :-)
May 28 2019
next sibling parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 20:26:50 UTC, Walter Bright wrote:
 On 5/28/2019 12:49 PM, Jonathan Marler wrote:
 You're right it's undecidable inside the function, but I think 
 it's decidable if you check it at the call site.
Yet the call site may also be doing attribute inference. Think of a graph with nodes in it, all interconnected with arbitrary edges, including cycles, and you have to find a set of attributes that satisfy each of the edges in it, when adding any attribute changes the topology of the graph. Even if there is an algorithm which can solve this, and I don't have a PhD in graph theory and have no idea if there is one or not: 1. there may be N solutions - which one is picked? 2. how do you explain to the user why? 3. how do you explain to the user when N is zero? 4. the combinatorics of this may mean it takes essentially infinite time 5. I have a hard enough time implementing/debugging the current bottom-up method 6. I've spent years dealing with problems with forward references, when there are cycles in the reference graph and incomplete types. Your proposal makes that infinitely worse. Like I said, it looks workable for trivial boundary cases, but that isn't how things work when people start using such a feature. So, no. Hell no :-)
Unfortunately my mathematical emphasis was on number theory and combinatorics...and it was only a bachelors :) But after sending my initial response I realized that if my example functions were also templates, then they would also have to use the allowGC = __traits(callSiteCompiles, new Object). In fact, in order to propogate the information, every template function that could eventually call one of these allowGC templates would also need to add the allowGC parameter to their template parameter list. Completely unscalable. That being said, even though the solution is unscalable and verbose, it does actually work. The compiler today is able to infer these attributes as demonstrated by the first version in my example code I showed earlier. And it will also work no matter how deep the rabit hole goes...turtles all the way down. It works because it determines the GC requirement top-down starting from the first non-template. Since non-templates do not infer the nogc attribute, the compiler already knows the answer to this inside each function that isn't a template. So we just need a way to propagate that information to each template instantiated inside that function which is what the template parameter allowGC did. We know this solution doesn't scale when using template parameters, but it would work if the compiler kept track of this internally. You'd just need to add a boolean variable to each template instance that indicates whether the caller allowed the GC or not, that is passed in when the template is instantiated. Anyway, I'm not saying we should do it, or even that we should do it this way, just saying it could be done. The math here is actually pretty simple, no need for a PhD this time.
May 28 2019
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/28/2019 2:12 PM, Jonathan Marler wrote:
 turtles all the way down.
Again, it is not that simple. Call graphs have cycles in them, they are not acyclic. Furthermore, since you suggested different code be instantiated based on inferred attributes, you have a FAR FAR more complex problem to resolve, since the graph connections change at every decision point.
May 28 2019
next sibling parent Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 21:41:28 UTC, Walter Bright wrote:
 On 5/28/2019 2:12 PM, Jonathan Marler wrote:
 turtles all the way down.
Again, it is not that simple. Call graphs have cycles in them, they are not acyclic. Furthermore, since you suggested different code be instantiated based on inferred attributes, you have a FAR FAR more complex problem to resolve, since the graph connections change at every decision point.
Again, in this case it is that simple. I'll explain it another way, maybe it will make sense this time. Attributes on functions inside template are inferred bottom-up, but templates are instantiated top-down. So trying to use inferred attributes to affect template instances is going to get you into a cyclic mess. In this case you are absolutely right. What I was suggesting is that you propagate the state as to whether or not the GC is allowed top-down, the same way templates are instantiated. You could do this today with template parameters, but like I said, it's not scalable because it requires every single template to add this parameter. It works because you are propagating this state in the same direction as you are instantiating the templates, top down, so you know before you even see the inside of the template whether or not the GC is allowed. So the argument that this creates a complex cyclic dependency resolution problem is not correct in the example I showed. However, there are other arguments against it, the main one I see being that you would essentially need to add "implicit template parameters". This would be a big change to the language with alot of consequences, something that would most certainly require a DIP and alot of research and thought, but, you shouldn't immediately dismiss and idea with "Hell No", especially when you don't fully understand it.
May 28 2019
prev sibling parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 21:41:28 UTC, Walter Bright wrote:
 On 5/28/2019 2:12 PM, Jonathan Marler wrote:
 turtles all the way down.
Again, it is not that simple. Call graphs have cycles in them, they are not acyclic. Furthermore, since you suggested different code be instantiated based on inferred attributes, you have a FAR FAR more complex problem to resolve, since the graph connections change at every decision point.
I read your response again and I think there's another piece you might be missing. You are correct that call graphs are not acyclic, however, the inference of attributes only applies to templated-functions, not regular functions. So we don't need to walk the entire graph to determine whether or not a template is allowed to use the GC, you only need to trace each template to it's nearest non-template function. Of course, this is just the theory, in practice it's just propagated as an implicit template parameter, a feature which is probably worth looking into and discussing on its own.
May 28 2019
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/28/2019 3:12 PM, Jonathan Marler wrote:
 You are correct that call graphs are not acyclic, however, the inference of 
 attributes only applies to templated-functions, not regular functions.  So we 
 don't need to walk the entire graph to determine whether or not a template is 
 allowed to use the GC, you only need to trace each template to it's nearest 
 non-template function.
Your algorithm will have to work for the worst case, which is all template code. This is hardly unreasonable, as Phobos is nearly entirely templates, with cyclical expansions. Worse, it's not just attribute inference, you proposed different code paths for different attributes, meaning the graph changes, too. It's a combinatorial explosion.
May 28 2019
next sibling parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
 On 5/28/2019 3:12 PM, Jonathan Marler wrote:
 You are correct that call graphs are not acyclic, however, the 
 inference of attributes only applies to templated-functions, 
 not regular functions.  So we don't need to walk the entire 
 graph to determine whether or not a template is allowed to use 
 the GC, you only need to trace each template to it's nearest 
 non-template function.
Your algorithm will have to work for the worst case, which is all template code. This is hardly unreasonable, as Phobos is nearly entirely templates, with cyclical expansions. Worse, it's not just attribute inference, you proposed different code paths for different attributes, meaning the graph changes, too. It's a combinatorial explosion.
Well, I didn't propose an algorithm. I was just providing a visual aid to your graph analogy. My suggesion has nothing to do with attribute inference on templates. If it did then you are completely right that this would be a combinatorial explosion. The idea is actually pretty "dumb" at its core. What's the simplest way to pass this information to a template? Just add a template parameter. foo(bool nogc, bool nothrow, bool pure,...)() { // logic to do something different based on template parameters } Then we need a way infer these attributes when we aren't inside a template. You can do this today with things like: foo!(__traits(compiles, new Object), __traits(compiles, throw new Object), ...)(); This function call is long, but the point of it is that this same call will pass the correct information based on whether the surrounding scope is nogc and/or nothrow. It infers the attributes before instantiating the template and passes the results to it. But keep in mind, it is not inferring attributes for the template, it is only passing information about the caller to the template which it may or may not use. But I think everyone can immediately see the problem with this. We can't add these parameters to every single template, and then add these __traits to every single template instantiation. There would be more boiler-plate than actual code! So came the idea of "implicit template parameters". The idea would be that any template could access a set of implicit variables to customize behavior. And to prevent an exponential explosion of template instances, you would only consider implicit template parameters when they are used, i.e. template foo() { static if (__traits(requiresNothrow)) auto foo() nothrow { ... } else auto foo() { ... } } void bar1() { foo(); } void bar2() nothrow { foo(); } When the compiler sees the `requiresNoThrow` trait being used inside a template, it notes that it is using one of the known implicit template parameters, meaning that it will be instantiated differently when the implicit "requiresNothrow" template parameter changes, just like it would if it was a normal template parameter. So in this example, the two calls to foo in bar1 and bar2 actually instantiate 2 versions of foo, even though they have the same "explicit template arguments", they have different "implicit template arguments". To understand how this works, note that implicit and explicit template parameters work the same way. So it would function just as if you had done this: template foo(bool nothrow) { static if (nothrow) auto foo() nothrow { ... } else auto foo() { ... } } void bar1() { foo!(__traits(compiles, new Object))(); } void bar2() nothrow { foo!(__traits(compiles, new Object))(); } It's just that now you don't need the boilerplate. Is it a good idea? I don't know, but I think it's worth some thought.
May 28 2019
parent reply aliak <something something.com> writes:
On Wednesday, 29 May 2019 at 02:45:37 UTC, Jonathan Marler wrote:
 On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
 [...]
Well, I didn't propose an algorithm. I was just providing a visual aid to your graph analogy. My suggesion has nothing to do with attribute inference on templates. If it did then you are completely right that this would be a combinatorial explosion. The idea is actually pretty "dumb" at its core. What's the simplest way to pass this information to a template? Just add a template parameter. [...]
Doesn't Jai do something similar to this? I.e. it passes in the "context" of the caller so that you can alter your code on what the environment is outside the function. So the context would could be a struct struct Conctext { bool isNogc; bool isSafe; // etc... } It's be akin to introducing the concept of a "compile time this" isn't it?
May 29 2019
parent Jonathan Marler <johnnymarler gmail.com> writes:
On Wednesday, 29 May 2019 at 12:39:16 UTC, aliak wrote:
 On Wednesday, 29 May 2019 at 02:45:37 UTC, Jonathan Marler 
 wrote:
 On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
 [...]
Well, I didn't propose an algorithm. I was just providing a visual aid to your graph analogy. My suggesion has nothing to do with attribute inference on templates. If it did then you are completely right that this would be a combinatorial explosion. The idea is actually pretty "dumb" at its core. What's the simplest way to pass this information to a template? Just add a template parameter. [...]
Doesn't Jai do something similar to this? I.e. it passes in the "context" of the caller so that you can alter your code on what the environment is outside the function. So the context would could be a struct struct Conctext { bool isNogc; bool isSafe; // etc... } It's be akin to introducing the concept of a "compile time this" isn't it?
Yes, last time I heard Jai was using sort of an implicit runtime parameter. However, I'm far from convinced it's a good idea at runtime . With implicit template parameters, they would only affect the template if they were used, so you only pay for it if you are using it. Not so with implicit runtime parameters.
May 29 2019
prev sibling parent reply Jonathan Marler <johnnymarler gmail.com> writes:
On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
 On 5/28/2019 3:12 PM, Jonathan Marler wrote:
 You are correct that call graphs are not acyclic, however, the 
 inference of attributes only applies to templated-functions, 
 not regular functions.  So we don't need to walk the entire 
 graph to determine whether or not a template is allowed to use 
 the GC, you only need to trace each template to it's nearest 
 non-template function.
Your algorithm will have to work for the worst case, which is all template code. This is hardly unreasonable, as Phobos is nearly entirely templates, with cyclical expansions. Worse, it's not just attribute inference, you proposed different code paths for different attributes, meaning the graph changes, too. It's a combinatorial explosion.
You do realize that all template code literally compiles to nothing right? A template isn't code, it's just a "template" that can be instantiated. If all you have is templates then there's nothing to instantiate them.
May 28 2019
parent reply Atila Neves <atila.neves gmail.com> writes:
On Wednesday, 29 May 2019 at 02:50:24 UTC, Jonathan Marler wrote:
 On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
 On 5/28/2019 3:12 PM, Jonathan Marler wrote:
 [...]
Your algorithm will have to work for the worst case, which is all template code. This is hardly unreasonable, as Phobos is nearly entirely templates, with cyclical expansions. Worse, it's not just attribute inference, you proposed different code paths for different attributes, meaning the graph changes, too. It's a combinatorial explosion.
You do realize that all template code literally compiles to nothing right? A template isn't code, it's just a "template" that can be instantiated. If all you have is templates then there's nothing to instantiate them.
I'm pretty sure Walter knows how templates work given that he's implemented them twice.
May 29 2019
parent Jonathan Marler <johnnymarler gmail.com> writes:
On Wednesday, 29 May 2019 at 09:50:13 UTC, Atila Neves wrote:
 On Wednesday, 29 May 2019 at 02:50:24 UTC, Jonathan Marler 
 wrote:
 On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
 [...]
You do realize that all template code literally compiles to nothing right? A template isn't code, it's just a "template" that can be instantiated. If all you have is templates then there's nothing to instantiate them.
I'm pretty sure Walter knows how templates work given that he's implemented them twice.
Yeah, no argument from me.
May 29 2019
prev sibling parent reply aliak <something something.com> writes:
On Tuesday, 28 May 2019 at 20:26:50 UTC, Walter Bright wrote:
 On 5/28/2019 12:49 PM, Jonathan Marler wrote:
 You're right it's undecidable inside the function, but I think 
 it's decidable if you check it at the call site.
Yet the call site may also be doing attribute inference. Think of a graph with nodes in it, all interconnected with arbitrary edges, including cycles, and you have to find a set of attributes that satisfy each of the edges in it, when adding any attribute changes the topology of the graph. Even if there is an algorithm which can solve this, and I don't have a PhD in graph theory and have no idea if there is one or not:
http://dev.stephendiehl.com/fun/006_hindley_milner.html ? And the so called Algorithm W. I'm nowhere close to understanding how it seems to work though. Swift uses an altered version: https://github.com/apple/swift/blob/master/docs/TypeChecker.rst I assume the solver magic is in here: https://github.com/apple/swift/blob/master/lib/Sema/CSSolver.cpp Rust used to use it but changed: http://smallcultfollowing.com/babysteps/blog/2014/07/09/an-experimental-new-type-inference-scheme-for-rust/ Though rust treats functions with different return types as the same definition and gives you an error. The thing you said about overloading on return types made me go and check around a bit. I already knew that swift does this at least. So I guess it isn't ambiguous since it's been implemented. But maybe it has something to do with the type system implementation they use? Cheers, - Ali
 1. there may be N solutions - which one is picked?
 2. how do you explain to the user why?
 3. how do you explain to the user when N is zero?
 4. the combinatorics of this may mean it takes essentially 
 infinite time
 5. I have a hard enough time implementing/debugging the current 
 bottom-up method
 6. I've spent years dealing with problems with forward 
 references, when there are cycles in the reference graph and 
 incomplete types. Your proposal makes that infinitely worse.

 Like I said, it looks workable for trivial boundary cases, but 
 that isn't how things work when people start using such a 
 feature.

 So, no. Hell no :-)
May 28 2019
parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Tuesday, 28 May 2019 at 22:33:46 UTC, aliak wrote:
 The thing you said about overloading on return types made me go 
 and check around a bit. I already knew that swift does this at 
 least. So I guess it isn't ambiguous since it's been 
 implemented. But maybe it has something to do with the type 
 system implementation they use?
It is possible, but when you have implicit type conversion... then you need to set up some strict presedence rules? Overloading over inferred attributes can be solved by a SAT solver, AFAIK, but that would require very good heuristics to perform well. The problem is most likely not in that complexity class, but simpler. Anyway, a solver can for each function find an expression for whether it is nogc or gc based on the ct parameters, so it is decidable. Same for nothrow.
May 28 2019
prev sibling next sibling parent Manu <turkeyman gmail.com> writes:
On Tue, May 28, 2019 at 11:10 AM Walter Bright via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On 5/28/2019 10:35 AM, Jonathan Marler wrote:
 After reading Walter's response here, another thought came to mind that instead
 of attribute overloading, a template with static-if could also be used.
nogc attributes are often inferred. Such constructs would make it undecidable.
I have never encountered a case with nogc where I would want to overload on attribute alone; the function signature is never the same between a nogc and uses-gc function.
May 28 2019
prev sibling parent KnightMare <black80 bk.ru> writes:
  nogc attributes are often inferred. Such constructs would make 
 it undecidable.
I doubt if nogc is needed to D. - it takes resources attempts to rewrite phobos/compiler for two different worlds. - we have good alternatives for that case: C++, rust, Go, Swift, ObjC.. - u can create almost same (x)strings, same int[]|double[]|real[], same maps/assoc.arrays with simple types(that (are|contains) no refs) through refcount. even slices can be rewritten that do addref/release for refs-containers - slice { ptrToContainer, index, length }. code can be same in text representation but incompatible at binary/obj - u cannot mix slices that point to GC-heap and using addref. BUT when include types with refs fields, when u have MOAO, when field can be Object obj.. well, compatibility is finish here - u should use refcount with cycling-breaker, u should use weakptr. it will be not D, it will be Swift/ObjC with "D"-frontend that need another separate RT lets say DiNoGC. why u want to do from D another Swift or C++ or rust? coz 0.1% lovers of bare metal want D for bare metal? but programmers for bare metal are 0.1% from all programmers and we have 0.0001%. we have totally 20kg of one programmer from 15k D-programmers (I threw dice) for whom u working, others use c++/rust/go. u can continue improve D/C++ Calypso https://github.com/Syniurge/Calypso remove GC from it and .. and u'll get strange C++ that knows 2 persons only. I can understand such job if no alternatives, somebody should be first, but we have alternatives with full RT already which grew up decades with a multimillion community for each alternative. D is good and unlike to others coz it have GC and it very close to metal(can do sse/assembler with high order functions) in one bottle (lets leave metaprogramming, UFCS, CTFE for ads slogans). u can say "without GC we can run D for WASM, for some hardware/arduino/raspberry". GC will appear in WASM. do u have RT for WASMwGC? ok, lets say another: how many people D-community will loose if will be declared "people, from today we finish works on nogc"? probably.. two men? D is good compiler, they cannot use strings, arrays, lambdas, well they can write own STL without GC. but they cannot use classes with polymorphism. with some allocator only. ok, lets write such allocator that supports some refcount and weakptr and again need another slices(with 3 fields). what to do with lambdas? same allocator too! imo such an allocator is very similar to GC. omg, we did cycle/loop to best DiNoGC and we are back to complicated version of slises, arrays/strings and classes and.. GC! again fking GC! that named aw/some allocator. ohh, no!). well, 2 men that want D on raspberry, I tell u and u will be grateful to me, go now to C++/Swift/Go, no need to cherish wet dreams about DiNoGC, u will get better world there, not with some crutches named "C++ without classes/D with structs/strange SwifD" Hallelujah!
May 28 2019
prev sibling next sibling parent user1234 <user1234 12.de> writes:
On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }

 This doesn't work, but on the face of it it's not ambiguous - 
 the second overload of fun() would not compile anyway.

 I was wondering whether allowing overloading on attributes in 
 general would be a good idea. I suspect templates and attribute 
 deduction make that difficult.
I'm afraid of non-intuitive or unexpected selection of overloads. Example during debugging someone realizes that "oh i see, this one is used actually".
May 28 2019
prev sibling next sibling parent reply Manu <turkeyman gmail.com> writes:
On Tue, May 28, 2019 at 9:10 AM Andrei Alexandrescu via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
     return fun(x);
 }

 This doesn't work, but on the face of it it's not ambiguous - the second
 overload of fun() would not compile anyway.

 I was wondering whether allowing overloading on attributes in general
 would be a good idea. I suspect templates and attribute deduction make
 that difficult.
I have wanted to overload on nothrow-ness before.
May 28 2019
parent Jonathan Marler <johnnymarler gmail.com> writes:
On Tuesday, 28 May 2019 at 21:53:13 UTC, Manu wrote:
 On Tue, May 28, 2019 at 9:10 AM Andrei Alexandrescu via 
 Digitalmars-d <digitalmars-d puremagic.com> wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
     return fun(x);
 }

 This doesn't work, but on the face of it it's not ambiguous - 
 the second overload of fun() would not compile anyway.

 I was wondering whether allowing overloading on attributes in 
 general would be a good idea. I suspect templates and 
 attribute deduction make that difficult.
I have wanted to overload on nothrow-ness before.
Yeah I could see some nice ways to select an implementation based on nothrow. struct CannotFail { bool failed() { return false; } } struct CanFail { bool _failed; bool failed() { return _failed; } } auto foo(bool mustBeNothrow)() { static if (mustBeNothrow) { // logic ... return CanFail(true); // or CanFail(false); } else { if (foo!true().failed) throw new Exception("an exception"); return CannotFail(); } } Now you can call foo from nothrow code or code that can throw. If the return value follows the same api in both cases, you can implement something like. if (foo.failed) { // do something // note: if this block can throw, then foo.failed will get reduced to "false" // so this whole block should be removed }
May 28 2019
prev sibling next sibling parent reply Kagamin <spam here.lot> writes:
On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }

 This doesn't work, but on the face of it it's not ambiguous - 
 the second overload of fun() would not compile anyway.

 I was wondering whether allowing overloading on attributes in 
 general would be a good idea. I suspect templates and attribute 
 deduction make that difficult.
It worked between 2.075 and 2.085, called the pure overload.
May 29 2019
parent Kagamin <spam here.lot> writes:
The fix was https://github.com/dlang/dmd/pull/9406
May 29 2019
prev sibling next sibling parent Stefan Koch <uplink.coder googlemail.com> writes:
On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }

 This doesn't work, but on the face of it it's not ambiguous - 
 the second overload of fun() would not compile anyway.

 I was wondering whether allowing overloading on attributes in 
 general would be a good idea. I suspect templates and attribute 
 deduction make that difficult.
That's a problematic idea. It means that adding an attribute on the caller can change the function that is called. Which seems exceedingly brittle. Arguing from a from a "principle of least surprise" standpoint. This is undesirable. - Stefan
May 29 2019
prev sibling parent reply Mike Franklin <slavo5150 yahoo.com> writes:
On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }
I think it really depends on the attribute. I haven't thought too much about the other attributes, but for `pure` I don't see the use case. If you can make the implementation of `fun(int)` pure, why would you need an additional impure implementation? Mike
May 29 2019
next sibling parent reply Manu <turkeyman gmail.com> writes:
On Wed, May 29, 2019 at 6:55 AM Mike Franklin via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu
 wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }
I think it really depends on the attribute. I haven't thought too much about the other attributes, but for `pure` I don't see the use case. If you can make the implementation of `fun(int)` pure, why would you need an additional impure implementation?
Right. nothrow is the only one that makes any sense at all to me, I've thought that overloading on nothrow might be useful once or twice. Otherwise... pure; like you say, nogc; signature is always different anyway, safe; makes no sense...
May 29 2019
next sibling parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Thursday, 30 May 2019 at 05:24:21 UTC, Manu wrote:
 Right. nothrow is the only one that makes any sense at all to 
 me, I've thought that overloading on nothrow might be useful 
 once or twice. Otherwise... pure; like you say,  nogc;
I wonder if overloading on nothrow could be better done as overloading on return value. Either you get "return value or exception" or you get "wrapped return value". What other use case is there?
May 30 2019
parent reply Manu <turkeyman gmail.com> writes:
On Thu, May 30, 2019 at 12:55 AM Ola Fosheim Grøstad via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On Thursday, 30 May 2019 at 05:24:21 UTC, Manu wrote:
 Right. nothrow is the only one that makes any sense at all to
 me, I've thought that overloading on nothrow might be useful
 once or twice. Otherwise... pure; like you say,  nogc;
I wonder if overloading on nothrow could be better done as overloading on return value. Either you get "return value or exception" or you get "wrapped return value". What other use case is there?
out params. wrapping return values kinda sucks, inhibits RVO. overloading on return value seems problematic... how would that work?
May 31 2019
parent reply Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Friday, 31 May 2019 at 17:52:28 UTC, Manu wrote:
 out params. wrapping return values kinda sucks, inhibits RVO.
It can, yes. It requires more advanced RVO optimizations to avoid it.
 overloading on return value seems problematic... how would that 
 work?
The compiler narrows down possible return types until one is left. The simple version would require an explicit type, a more advanced version would look at how the result is used and use that to narrow down the candidate types until one is left.
May 31 2019
parent reply Manu <turkeyman gmail.com> writes:
On Fri, May 31, 2019 at 11:35 AM Ola Fosheim Grøstad via Digitalmars-d
<digitalmars-d puremagic.com> wrote:
 On Friday, 31 May 2019 at 17:52:28 UTC, Manu wrote:
 out params. wrapping return values kinda sucks, inhibits RVO.
It can, yes. It requires more advanced RVO optimizations to avoid it.
 overloading on return value seems problematic... how would that
 work?
The compiler narrows down possible return types until one is left. The simple version would require an explicit type, a more advanced version would look at how the result is used and use that to narrow down the candidate types until one is left.
A fun() { return A(); } B fun() { return B(); } fun(); // <- return value ignored ...?
May 31 2019
next sibling parent Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
On Friday, 31 May 2019 at 21:39:10 UTC, Manu wrote:
 On Fri, May 31, 2019 at 11:35 AM Ola Fosheim Grøstad via 
 Digitalmars-d <digitalmars-d puremagic.com> wrote:
 On Friday, 31 May 2019 at 17:52:28 UTC, Manu wrote:
 out params. wrapping return values kinda sucks, inhibits RVO.
It can, yes. It requires more advanced RVO optimizations to avoid it.
 overloading on return value seems problematic... how would 
 that work?
The compiler narrows down possible return types until one is left. The simple version would require an explicit type, a more advanced version would look at how the result is used and use that to narrow down the candidate types until one is left.
A fun() { return A(); } B fun() { return B(); } fun(); // <- return value ignored ...?
Ambiguous. cast(A)fun(); to call that specific overload. I guess that means cast(void)cast(A)fun(); if fun is pure... -- Simen
May 31 2019
prev sibling parent Ola Fosheim =?UTF-8?B?R3LDuHN0YWQ=?= <ola.fosheim.grostad gmail.com> writes:
On Friday, 31 May 2019 at 21:39:10 UTC, Manu wrote:
 A fun() { return A(); }
 B fun() { return B(); }

 fun(); // <- return value ignored

 ...?
In this instance it is a matter of "ideological" language semantics. If the language "ideology" is spec-driven then compiler should be allowed to choose the one that does less work, given that they both are supposed to implement the same spec. The IDE can show which one is chosen, so I don't think it is as big a problem as people make it out to be (assuming you limit your imports). If the language has error-as-return-value as an idiom, then it might require the caller to check all returned errors. In that case it should pick the function call that throws if the caller does not use the returned value, and complain if both versions of "fun()" returns an error value. Anyway, you can get around it by: A _ = fun(); // _ is not used or: returns!A( fun() ); // wrapper that ensures the return type to be of A
May 31 2019
prev sibling parent reply Exil <Exil gmall.com> writes:
On Thursday, 30 May 2019 at 05:24:21 UTC, Manu wrote:
 On Wed, May 29, 2019 at 6:55 AM Mike Franklin via Digitalmars-d 
 <digitalmars-d puremagic.com> wrote:
 On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
 wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }
I think it really depends on the attribute. I haven't thought too much about the other attributes, but for `pure` I don't see the use case. If you can make the implementation of `fun(int)` pure, why would you need an additional impure implementation?
Right. nothrow is the only one that makes any sense at all to me, I've thought that overloading on nothrow might be useful once or twice. Otherwise... pure; like you say, nogc; signature is always different anyway, safe; makes no sense...
I don't think it is that useful. Consider if you did do the following: void foo() nothrow; void foo(); void bar() nothrow { foo(); // ok useful calls foo() nothrow } void ree() { foo(); // ambiguous can call both, should be an error } It can call either function if the callee isn't nothrow. I don't think it should implicitly call the function without the nothrow either. It really varies on the situation which one should be called, you may want the variant that doesn't throw an exception. It would be surprising if you have a function defined as nothrow, then add an overload without the nothrow and now it is now calling a different overload in functions without nothrow. At which point this isn't all that useful, it will work as expected in nothrow functions but then you will have to explicitly choose which one to use otherwise. Which is basically the situation now with nothrow, you just have to use a different function name.
May 30 2019
parent reply Q. Schroll <qs.il.paperinik gmail.com> writes:
On Thursday, 30 May 2019 at 13:58:20 UTC, Exil wrote:
 I don't think it is that useful. Consider if you did do the 
 following:

     void foo() nothrow;
     void foo();

     void bar() nothrow
     {
          foo(); // ok useful calls foo() nothrow
     }

     void ree()
     {
         foo(); // ambiguous can call both, should be an error
     }


 It can call either function if the callee isn't nothrow. I 
 don't think it should implicitly call the function without the 
 nothrow either. It really varies on the situation which one 
 should be called, you may want the variant that doesn't throw 
 an exception. It would be surprising if you have a function 
 defined as nothrow, then add an overload without the nothrow 
 and now it is now calling a different overload in functions 
 without nothrow. At which point this isn't all that useful, it 
 will work as expected in nothrow functions but then you will 
 have to explicitly choose which one to use otherwise. Which is 
 basically the situation now with nothrow, you just have to use 
 a different function name.
I think is is orthogonal to how `const` is used on non-static member functions: struct Example { void member() const { } void member() /*mutable*/ { } } Example ex; ex.member(); // calls mutable It could call either function if the callee is mutable. People all over the board think it should implicitly call the function without the const. It could vary on the situation which one should be called, but mostly, you may want the variant that uses mutability to its advantage. Nobody finds it surprising if you have a const method, then add a mutable overload and now it is now calling a different overload in functions where the object is mutable. You could handle mutable and const methods the same: Tell people they need different names. It just doesn't look so nice with operators (fixed names, you cannot choose another), type inference and meta-programming. Nobody sees a problem with that, and it's about the same thing: The context of the call decides which overload is chosen. It is nothing new, C++ does it since the beginning. As I see it, this is can be made into an argument for a `nothrow(some_ct_bool_value)` attribute.
May 30 2019
parent reply Exil <Exil gmall.com> writes:
On Thursday, 30 May 2019 at 15:30:26 UTC, Q. Schroll wrote:
 On Thursday, 30 May 2019 at 13:58:20 UTC, Exil wrote:
 I don't think it is that useful. Consider if you did do the 
 following:

     void foo() nothrow;
     void foo();

     void bar() nothrow
     {
          foo(); // ok useful calls foo() nothrow
     }

     void ree()
     {
         foo(); // ambiguous can call both, should be an error
     }


 It can call either function if the callee isn't nothrow. I 
 don't think it should implicitly call the function without the 
 nothrow either. It really varies on the situation which one 
 should be called, you may want the variant that doesn't throw 
 an exception. It would be surprising if you have a function 
 defined as nothrow, then add an overload without the nothrow 
 and now it is now calling a different overload in functions 
 without nothrow. At which point this isn't all that useful, it 
 will work as expected in nothrow functions but then you will 
 have to explicitly choose which one to use otherwise. Which is 
 basically the situation now with nothrow, you just have to use 
 a different function name.
I think is is orthogonal to how `const` is used on non-static member functions: struct Example { void member() const { } void member() /*mutable*/ { } } Example ex; ex.member(); // calls mutable It could call either function if the callee is mutable. People all over the board think it should implicitly call the function without the const. It could vary on the situation which one should be called, but mostly, you may want the variant that uses mutability to its advantage. Nobody finds it surprising if you have a const method, then add a mutable overload and now it is now calling a different overload in functions where the object is mutable. You could handle mutable and const methods the same: Tell people they need different names. It just doesn't look so nice with operators (fixed names, you cannot choose another), type inference and meta-programming. Nobody sees a problem with that, and it's about the same thing: The context of the call decides which overload is chosen. It is nothing new, C++ does it since the beginning. As I see it, this is can be made into an argument for a `nothrow(some_ct_bool_value)` attribute.
It is not orthogonal at all. If you want to look at it this way. It looks like it is othrogonal to nothrow because of the syntactic sugar. What the actual definition is this: void member( ref const(Example) _this ); void member( ref Example _this ); It is part of the parameters, which is perfectly legal even as a static function not part of an object. Yes you can call either, but one is an exact type match and is chosen instead. Looking at nothrow now, it doesn't have any parameters that can be used to evaluate the function. void foo() nothrow; void foo(); In the case of const for an object, it is just syntactic sugar for an already existing feature that follows the rules for function selection. This proposed changed will be adding another layer to that, and it will be quite messy due to how templates auto infer their attributes. If you want to draw parallels with C++, you can't define a function as nothrow and non-nothrow in C++ (noexcept). It has to be defined as either one or the other.
May 31 2019
parent reply Exil <Exil gmall.com> writes:
Found some odd behavior, seems if a template needs to reference 
itself, it does not add the  nogc attribute to itself. Meaning 
it's type is not  nogc/nothrow, but it can still be called in 
 nogc functions. Unless you take a pointer to it, then it doesn't 
have the attributes so it can't be called.

https://run.dlang.io/is/YJDs7y

import std.stdio;

void test3(t)() {
     import core.stdc.stdio;
     printf("%s\n", t.stringof.ptr);
}

void test2(bool check)()  {
     static if(check) test3!(typeof(&test2))();
     else              test3!(int)();
}

void main()  nogc {

     auto p1 = &test2!false;
     auto p2 = &test2!true;
	
     test2!false();
     p1();

     test2!true(); // ok
     // p2(); // error can't call  nogc function

     printf("---\n%s\n%s\n", typeof(p1).stringof.ptr, 
typeof(p2).stringof.ptr);
}
Jun 01 2019
parent Jonathan Marler <johnnymarler gmail.com> writes:
On Saturday, 1 June 2019 at 12:08:50 UTC, Exil wrote:
 Found some odd behavior, seems if a template needs to reference 
 itself, it does not add the  nogc attribute to itself. Meaning 
 it's type is not  nogc/nothrow, but it can still be called in 
  nogc functions. Unless you take a pointer to it, then it 
 doesn't have the attributes so it can't be called.

 [...]
I've made an issue here: https://issues.dlang.org/show_bug.cgi?id=19934
Jun 01 2019
prev sibling parent Kagamin <spam here.lot> writes:
On Wednesday, 29 May 2019 at 13:53:37 UTC, Mike Franklin wrote:
 On Tuesday, 28 May 2019 at 16:08:38 UTC, Andrei Alexandrescu 
 wrote:
 int fun(int) pure;
 int fun(int);

 pure int gun(int x)
 {
    return fun(x);
 }
I think it really depends on the attribute. I haven't thought too much about the other attributes, but for `pure` I don't see the use case. If you can make the implementation of `fun(int)` pure, why would you need an additional impure implementation?
A use case I found is that the pure overload can use GC to allocate memory for CTFE, while the impure overload can use a custom allocator for run time. The compiler always chose pure overload, so I gave up on purity altogether.
May 30 2019