www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Value-based function overloading - Take Two

reply Nod <Nod_member pathlink.com> writes:
Hey, this went well! There has been lots of feedback, and I love feedback :)
Ranges for switch statements[1] have been debated, and concerns regarding code
readability and debuggability[2] has been aired. Mostly, people are positive
about the idea.

Spurred by the generally positive attitude towards this idea, I will here
present an addendum to the original draft, further specifying and explaining
some of the dark areas, and integrating some of the feedback.

For reference, the original draft is here:
http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23796

/****************************************
* Defaults
***/
I didn't touch on it in the previous draft, and perhaps it is obvious, but just
for the record: When there is no range specified for a function overload, that
overload is used as a default, and is not ambiguous with any overload which has
a range specified.

Example:
void foo(int a in 0..5) { ... }
void foo(int a) { ... }  // legal, default

There can only be one default per parameter, more than one is ambiguous and an
error.

Example:
void foo(int a) { ... }
void foo(int a) { ... }  // illegal, duh

Example:
void foo(int a in 0..4, char b in 'x') { ... }
void foo(int a,         char b in 'y') { ... }  // legal
void foo(int a in 5..9, char b       ) { ... }  // legal
void foo(int a,         char b in 'z') { ... }  // illegal 
void foo(int a,         char b       ) { ... }  // very illegal

/****************************************
* Shadowing
***/
Implicitly, it is not possible to override overloads. That is, one cannot
accidentally create an overload which shadows another overload. This is ensured
by not allowing range ambiguities.

Explicitly, however, this can be done. By using the 'override' attribute, the
function will get a higher precedence than the original overload, thus resolving
any ambiguity which may exist.

Example:
void foo(int a in 0..10) { ... }
void foo(int a in 4..5) { ... }  // illegal, ambiguous
override void foo(int a in 4..5) { ... }  // legal, has higher precedence

Example:
void foo(int a in 0..10) { ... }
override void foo(int a in 4..5) { ... }

int main()
{
foo(8);  // calls first
foo(4);  // calls second
}

If the override attribute is used, the original function must exist, e.g:
override void foo(int a in 0..10) { ... }  // illegal, nothing to override

Overriding overridden overloads:
This is only possible if the already-overridden overload resides in another,
imported module.

Example:
void foo(int a in 0..10) { ... }
override void foo(int a in 0..5) { ... }  // legal
override void foo(int a in 0..3) { ... }  // illegal, same module

Example:

mod.d
void foo(int a in 0..10) { ... }
override void foo(int a in 0..5) { ... }  // legal

main.d
import mod;
override void foo(int a in 0..3) { ... }  // legal

Overriding overloads is discouraged, and should be done only when absolutely
necessary.

/****************************************
* Scoping
***/
Without the override keyword, regular module symbol ambiguity resolution apply.
That is, symbol ambiguities are illegal, and one must use the full module name
in the case of a symbol clash.

Example:

mod.d
void foo(int a in 5..20) { ... }

main.d
import mod;
void foo(int a in 0..15) { ... }

int main()
{
foo(10);  // calls foo in main.d
foo(20);  // illegal, foo in main.d cannot handle that value
mod.foo(10);  // calls foo in mod.d
mod.foo(0);  // illegal, foo in mod.d cannot handle that value
}

When the override keyword is used however, and no original function exists in
the current module, the overload in the imported module gets overridden, and no
symbol clash occurs. The overload gets "merged" with the imported module.

Example:

mod.d
void foo(int a in 5..20) { ... }

main.d
import mod;
override void foo(int a in 0..15) { ... }

int main()
{
foo(10);  // calls foo in main.d
foo(20);  // calls foo in mod.d
foo(0);  // calls foo in main.d
}

If there both exists an original function of the same name in the current
module, as well as one in an imported module, then the one in the current module
gets overridden. The function in the module then cannot be overridden. This
situation should be rare enough to not require an easy solution.

/****************************************
* In closing...
***/
I have tried to keep the technique as generally useful as possible, and since I
value programmer freedom, I have done little to bar potential misuse[3]. This
may not be liked by all, and it may not be the right way. Do tell me what you
think.

I really need to get some sleep now.
-Nod-


[1] http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23856
[2] http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23815
[3] The following exchange with Hasan Aljudy expands on that topic.
H.A: http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23815
Me: http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23826
H.A: http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23841
Me: http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23853
May 18 2005
parent reply "Andrew Fedoniouk" <news terrainformatica.com> writes:
 Example:
 void foo(int a in 0..5) { ... }
 void foo(int a) { ... }  // legal, default
Probably I don't understand hidden treasures of the approach... If I need this I would do: alias void function(int) foo; static int selector[] = [int.min, 0, 5, int.max]; static foo foos[] = [ function void(int a) { .....}, function void(int a) { .....}, function void(int a) { .....} ]; select!(selector,foos); And I can manage by myself what is better to use: simple if-else-if, bsearch or just table in select(). Andrew.
May 18 2005
next sibling parent reply "Craig Black" <cblack ara.com> writes:
It's just like any other high-level feature.  It can always be implemented 
using low level code, but it's not as clean and convenient.  Less code == 
greater maintainability

-Craig

"Andrew Fedoniouk" <news terrainformatica.com> wrote in message 
news:d6hcgg$15ah$1 digitaldaemon.com...
 Example:
 void foo(int a in 0..5) { ... }
 void foo(int a) { ... }  // legal, default
Probably I don't understand hidden treasures of the approach... If I need this I would do: alias void function(int) foo; static int selector[] = [int.min, 0, 5, int.max]; static foo foos[] = [ function void(int a) { .....}, function void(int a) { .....}, function void(int a) { .....} ]; select!(selector,foos); And I can manage by myself what is better to use: simple if-else-if, bsearch or just table in select(). Andrew.
May 19 2005
parent reply "Andrew Fedoniouk" <news terrainformatica.com> writes:
"Craig Black" <cblack ara.com> wrote in message 
news:d6idif$23h7$1 digitaldaemon.com...
 It's just like any other high-level feature.  It can always be implemented 
 using low level code, but it's not as clean and convenient.  Less code == 
 greater maintainability

 -Craig
" Less code == greater maintainability" Well, it depends.... In this particular case, I wouldn't even try to maintain code having something like this: module A; void foo(int a in 0..5) { ... } module B; void foo(int a in 5..100) { ... } Andrew.
 "Andrew Fedoniouk" <news terrainformatica.com> wrote in message 
 news:d6hcgg$15ah$1 digitaldaemon.com...
 Example:
 void foo(int a in 0..5) { ... }
 void foo(int a) { ... }  // legal, default
Probably I don't understand hidden treasures of the approach... If I need this I would do: alias void function(int) foo; static int selector[] = [int.min, 0, 5, int.max]; static foo foos[] = [ function void(int a) { .....}, function void(int a) { .....}, function void(int a) { .....} ]; select!(selector,foos); And I can manage by myself what is better to use: simple if-else-if, bsearch or just table in select(). Andrew.
May 19 2005
next sibling parent reply "Craig Black" <cblack ara.com> writes:
Let me try to follow your logic.  "If a language feature allows for new ways 
write bad code, then this language feature should not be considered."  Is 
this what you are thinking here?  There are always ways to write bad code 
and misuse language features.  That doesn't mean that these language 
features are bad.

-Craig

"Andrew Fedoniouk" <news terrainformatica.com> wrote in message 
news:d6ie7d$2473$1 digitaldaemon.com...
 "Craig Black" <cblack ara.com> wrote in message 
 news:d6idif$23h7$1 digitaldaemon.com...
 It's just like any other high-level feature.  It can always be 
 implemented using low level code, but it's not as clean and convenient. 
 Less code == greater maintainability

 -Craig
" Less code == greater maintainability" Well, it depends.... In this particular case, I wouldn't even try to maintain code having something like this: module A; void foo(int a in 0..5) { ... } module B; void foo(int a in 5..100) { ... } Andrew.
 "Andrew Fedoniouk" <news terrainformatica.com> wrote in message 
 news:d6hcgg$15ah$1 digitaldaemon.com...
 Example:
 void foo(int a in 0..5) { ... }
 void foo(int a) { ... }  // legal, default
Probably I don't understand hidden treasures of the approach... If I need this I would do: alias void function(int) foo; static int selector[] = [int.min, 0, 5, int.max]; static foo foos[] = [ function void(int a) { .....}, function void(int a) { .....}, function void(int a) { .....} ]; select!(selector,foos); And I can manage by myself what is better to use: simple if-else-if, bsearch or just table in select(). Andrew.
May 19 2005
next sibling parent "Andrew Fedoniouk" <news terrainformatica.com> writes:
"Craig Black" <cblack ara.com> wrote in message 
news:d6ien1$24kv$1 digitaldaemon.com...
 Let me try to follow your logic.  "If a language feature allows for new 
 ways write bad code, then this language feature should not be considered." 
 Is this what you are thinking here?  There are always ways to write bad 
 code and misuse language features.  That doesn't mean that these language 
 features are bad.

 -Craig
:) My statement was not so generalizing as yours... I'll try to explain differently what I mean then: See: if you have switch(...) { case....} or the like you can be 100% sure that all options are there and nothing missed. In case of set of void foo(int a in 5..100) { ... } you cannot be sure. E.g. somebody forgot to include module B in the makefile. Even in one file it will be difficult (well, not so obvious) to find all cases. And these are exactly "maintainance problems". Andrew.
 "Andrew Fedoniouk" <news terrainformatica.com> wrote in message 
 news:d6ie7d$2473$1 digitaldaemon.com...
 "Craig Black" <cblack ara.com> wrote in message 
 news:d6idif$23h7$1 digitaldaemon.com...
 It's just like any other high-level feature.  It can always be 
 implemented using low level code, but it's not as clean and convenient. 
 Less code == greater maintainability

 -Craig
" Less code == greater maintainability" Well, it depends.... In this particular case, I wouldn't even try to maintain code having something like this: module A; void foo(int a in 0..5) { ... } module B; void foo(int a in 5..100) { ... } Andrew.
 "Andrew Fedoniouk" <news terrainformatica.com> wrote in message 
 news:d6hcgg$15ah$1 digitaldaemon.com...
 Example:
 void foo(int a in 0..5) { ... }
 void foo(int a) { ... }  // legal, default
Probably I don't understand hidden treasures of the approach... If I need this I would do: alias void function(int) foo; static int selector[] = [int.min, 0, 5, int.max]; static foo foos[] = [ function void(int a) { .....}, function void(int a) { .....}, function void(int a) { .....} ]; select!(selector,foos); And I can manage by myself what is better to use: simple if-else-if, bsearch or just table in select(). Andrew.
May 19 2005
prev sibling parent Benji Smith <dlanguage xxagg.com> writes:
Craig Black wrote:
 Let me try to follow your logic.  "If a language feature allows for new ways 
 write bad code, then this language feature should not be considered."  Is 
 this what you are thinking here?  There are always ways to write bad code 
 and misuse language features.  That doesn't mean that these language 
 features are bad.
 
 -Craig
If a new language feature... 1) Doesn't really solve any existing problem with the language, but rather just provides new syntax for something that's already possible. AND 2) Creates the opportunity to write confusing code with hidden dependencies and invisible control-flow logic. THEN Yes, I would prefer that such a language feature should not be considered. Yeah, there are always ways to write bad code, but it seems like we should minimize those cases, except in circumstances where the new feature actually ADDS SOMETHING IMPORTANT to the language. --BenjiSmith
May 19 2005
prev sibling parent Nod <Nod_member pathlink.com> writes:
In article <d6ie7d$2473$1 digitaldaemon.com>, Andrew Fedoniouk says...
"Craig Black" <cblack ara.com> wrote in message 
news:d6idif$23h7$1 digitaldaemon.com...
 It's just like any other high-level feature.  It can always be implemented 
 using low level code, but it's not as clean and convenient.  Less code == 
 greater maintainability

 -Craig
" Less code == greater maintainability" Well, it depends.... In this particular case, I wouldn't even try to maintain code having something like this: module A; void foo(int a in 0..5) { ... } module B; void foo(int a in 5..100) { ... } Andrew.
 <snip>
I agree that that *would* be a maintenance problem. However, any feature can be misused, and doing what you suggest should be highly discouraged. And that should read: module A; void foo(int a in 0..5) { ... } module B; override void foo(int a in 5..100) { ... } Read the spec ye lazy ass flame-fountains! :)
May 19 2005
prev sibling parent Nod <Nod_member pathlink.com> writes:
In article <d6hcgg$15ah$1 digitaldaemon.com>, Andrew Fedoniouk says...
 Example:
 void foo(int a in 0..5) { ... }
 void foo(int a) { ... }  // legal, default
Probably I don't understand hidden treasures of the approach... If I need this I would do: alias void function(int) foo; static int selector[] = [int.min, 0, 5, int.max]; static foo foos[] = [ function void(int a) { .....}, function void(int a) { .....}, function void(int a) { .....} ]; select!(selector,foos); And I can manage by myself what is better to use: simple if-else-if, bsearch or just table in select(). Andrew.
The hidden treasures lie in easing maintenance and refactoring. Consider for example splitting a function - turning it into two, because the first one has gotten too complex. In the normal case, one would have to find all uses of the function, and exchange it for an if/else or switch/case, or indeed - your little snippet. This can be nontrivial for a large project, and if you're not familiar with the codebase, even possibly hazardous. In the case of value-based overloading, you simply split the function right there and then, and no other code needs to be touched. The overload redirection is inserted by the compiler, and all other code is oblivious to the change. More generally speaking, by transferring the decision of which values a function can handle out of the code and into the function, the code becomes more modular. It is true one could simply use the following construct to successfully emulate the behaviour: void foo(int a) { if (a < 0) { ... } else if (a >= 0) { ... } } But then the code is still *fixed in its framework* so to speak; you lose forward referencing, and the ability to easily move code about. And it isn't pretty either :) -Nod-
May 19 2005