www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Value-based function overloading - A Step Back

reply Nod <Nod_member pathlink.com> writes:
Sorry, no new draft today. I think I am running out of specifics to specify and
unclarities to (un)clarify :)

None the less, hoping not to disappoint all you supporters[1] out there, and on
the basis of some recent hints[2], I will instead present a high-level overview,
avoiding technical details. 

I may omit some details in favour of clarity. If you would like a more in-depth
view of any of the subjects, check out the corresponding draft.

1) Summary, Basic syntax, Ambiguity resolution
:  http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23796
2) Defaults, Shadowing/Overriding, Scoping
:  http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23897
3) Inner/Nested functions, Classes, Templates
:  http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/23966
4) Mixins and Ranges
:  http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/24061

/****************************************
* When viewed from the sky, everything looks small
***/
This has turned out to be more than one proposal. Actually there are five
distinct ones, all intertwined in the drafts. By themselves, they are really
simple. Together they create a greater whole. Those proposals are:

* Generic range support
* The "in" keyword
* Function overriding
* Value-based function overloading
* Value-based template/mixin specialization

I will briefly cover each one in turn, and only thereafter explain how they fit
together.

/****************************************
* 1) Generic range support
***/
In D, one can use distinct values such as 42, 'c', 1.2. Sometimes though, it is
necessary to specify a *range* of values. Partial range support is already
supported in D through array slices. This proposal extends range support to
anywhere where it makes sense. This includes switch statements and array
initialization.

Example:
:switch (i)
:{
:case 0..4: ... break;
:case 5..9: ... break;
:default:
:}

Example:
:char[] = 'a'..'e';  // "abcde"

Ranges does not have to be solid, they can have holes in them. It is possible to
use the bitwise operators to join &, subtract |, take difference ^ or negate !
ranges. Range literals are all inclusive[3].

* Benefits: More flexibility; less limitations. Less code clutter.
* Hazards: Compiler complexity. Code performance may be less obvious.

/****************************************
* 2) The "in" keyword
***/
To be able to use ranges with if-else statements we must be able to test if a
single value exists within a range. We choose the "in" keyword for this use.

Example:
:if (i in 1..5) { ... }

Classes and structs can define the opIn method to behave like a range.

* Benefits: Clearer code. More intuitive code - e.g. "foo" in Dictionary, "foo"
in String, etc. May extend well to introspection(2.0); testing e.g. method in
class.
* Hazards: Compiler complexity. Parser complexity by overloading "in".

/****************************************
* 3) Function overriding
***/
The "override" attribute is normally used for making sure a function in a
derived class really overrides a function in a base class[4]. This proposal
extends that attribute to be usable anywhere where it makes sense.

Overriding a function simply means that the overrider gets a higher priority
than the overridee, in terms of overload resolution. With regular functions, the
result is that the overrider completely "shadows" the overridee.

Example:
:override void foo() { ... }  // this one has higher priority
:void foo() { ... }  // so you can't call this one
:int main()
:{
:  foo();  // calls first
:}

Due to forward referencing it is only possible to override a function in the
same scope *once*. However, as soon as we enter a new scope, we can do another
override. This should be used with care though.

* Benefits: More consistency/flexibility, less limitations. Allows one-line
drop-in wrappers/replacements/modifiers to be created, targeting any scope, thus
easing development/debugging.
* Hazards: Compiler complexity. Usage looks like a hack solution. Irresponsible
use can cause complex code.

/****************************************
* 4) Value-based function overloading
***/
This overloads a function based on the values of its parameters.

Example:
:void foo(int i in 0..5) { ... }  // specialization
:void foo(int i) { ... }  // default
:int main()
:{
:  foo(3);  // calls first
:  foo(7);  // calls second
:}

Ambiguities are illegal. That is, there can be no overlap between ranges of
different overloads. Well, except for the defaults of course; those are special
cases.

* Benefits: Can in some cases give clearer, more maintainable code. Makes code
more modular, by moving flow control logic to the function.
* Hazards: Compiler complexity. Irresponsible use.

/****************************************
* 5) Value-based template/mixin specialization
***/
This specializes a template or mixin based on the values of the parameters.

Example:
:template TFoo (T, int Size in 0..10) { ... }  // specialization
:template TFoo (T, int Size) { ... }  // default
:int main()
:{
:  TFoo foo = new TFoo!(double, 5);  // instanciates the first one
:  TFoo foo = new TFoo!(double, 15);  // instanciates the second one
:}

The same rules apply here, no ambiguities allowed. Value-based template/mixin
specialization is roughly the same as value-based function overloading, but
operates on a higher level.

* Benefits: Roughly the same as value-based function overloading.
* Hazards: Same as value-based function overloading.

/****************************************
* The big picture
***/
The thigh-bone's connected to the.. hip-bone...

You have already seen how the "in" keyword relates to ranges, and how ranges are
used in value-based overloads, so I won't cover that again.

Function overriding, when used in conjunction with value-based overloading gives
the ability to override only a specific range of values. 

Example:
mod.d
:void foo(int i) { ... }

main.d
:override void foo(int i in 0..5) { ... }
:int main()
:{
:  import mod;
:  foo(7);  // calls mod.foo
:  foo(2);  // calls main.foo
:}

This can be used to e.g. work around a bug or log function calls.

One can of course also do this by wrapping, or by modifying the original
function. The benefit of using overriding is that you don't need to do any
modifications to either function or function invocations, reducing the risk for
error.

What cannot be done by wrapping, and neither by modifying the original function
is to override a function in only a specific scope. To normally do this, you
would have to change all of the function invocations in that scope. Function
overriding makes this easy.

Example:
mod.d
:void foo(int i) { ... }

main.d
:int main()
:{
:  import mod;
:  void bar()
:  {
:    override void foo(int i in 0..5) { ... }
:    foo(7);  // calls mod.foo
:    foo(2);  // calls main.bar.foo
:  }
:  foo(7);  // calls mod.foo
:  foo(2);  // calls mod.foo
:}

Neat, huh? By applying logging or wrappers to specific sections of code, it
becomes simpler to track down bugs and develop fixes.

For some more examples and idioms, check out the drafts.

/****************************************
* Is it really feasible for D?
***/
I think so. It fits well ideologically by making code clearer, reducing bugs.
Excluding compiler complexity and name mangling issues, two areas that I just
don't know enough about, I think it also fits technically. Alas, it is not my
choice to make; Walter will have the final say, as is his privilege.

/****************************************
* Easier to chew?
***/
I hope you now have a more clear understanding of what value-based overloading
is about, and find that it's not really so complex once you break it down. The
idioms presented here are surely not the only ones, and in time more will be
discovered. These enhancements gives us tools to be more creative in an already
great language. Not without a certain degree of caution of course, but I believe
that the hazards can be avoided by means of documentation and good coding
practises.

If you still don't quite get it, put on the hat[5], and don't be afraid to ask
questions :)

*phew* -Nod-

[1] http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/24076
[2] http://www.digitalmars.com/drn-bin/wwwnews?digitalmars.D/24086
[3] Ranges are all inclusive. This is inconsistent with the way ranges work in
array slicing. Either the one or the other needs to be changed for consistency.
[4] http://www.digitalmars.com/d/attribute.html
[5] The hat:
:      *
:     / \
:    /   \
:   /     \
:  /   ?   \
: /_________\
May 22 2005
parent reply Derek Parnell <derek psych.ward> writes:
On Mon, 23 May 2005 05:06:05 +0000 (UTC), Nod wrote:

 Sorry, no new draft today. I think I am running out of specifics to specify and
 unclarities to (un)clarify :)
 
 None the less, hoping not to disappoint all you supporters[1] out there, and on
 the basis of some recent hints[2], I will instead present a high-level
overview,
 avoiding technical details. 
 
 I may omit some details in favour of clarity. If you would like a more in-depth
 view of any of the subjects, check out the corresponding draft.
I have hung back on these discussion because I wasn't quite sure what was actually being put forward nor did I know what I felt about things, so far as I did recognize the proposals. Anyhow, now with this clear proposal request, I think I do fully understand it. But now I have to sit back and say "what benefit is this to D?". And I can't think of much. It seems like a whole lot of work and finessing to cater for rare situations, that can currently be taken care of in other ways. [snip]
 
 /****************************************
 * 1) Generic range support
 ***/
 In D, one can use distinct values such as 42, 'c', 1.2. Sometimes though, it is
 necessary to specify a *range* of values. Partial range support is already
 supported in D through array slices. This proposal extends range support to
 anywhere where it makes sense. This includes switch statements and array
 initialization.
Now this one seems to be the most beneficial because ranges of literal values are used with some regularity. So it might be an occasional time saver.
 Example:
:switch (i)
 :{
:case 0..4: ... break;
:case 5..9: ... break;
 :default:
 :}
 
The proposal however only talks about a subset of range types. In the case of 'switch' statements, an alternate to this range could allow many more types of ranges... switch (i) { case >=0 && <= 4: ... break; case >=5 && <= 9: ... break; case >9 && < 20: ... break; case 20, 21, 24, 29: ... break; case >= 22 && !(24,29): ... break; case <50: break; default: } A 'range' could even be made similar to enums. That is, we could have named ranges to assist readibilty. range int patA = {>=0 && <=4}; . . . switch(i) { case patA: . . .}
 Example:
:char[] = 'a'..'e';  // "abcde"
Doesn't this assume a collating sequence based on ASCII. What do you do with non-ASCII characters?
 Ranges does not have to be solid, they can have holes in them. It is possible
to
 use the bitwise operators to join &, subtract |, take difference ^ or negate !
 ranges. 
How would you code holes? 0 .. 10 except 6?
Range literals are all inclusive[3].
As you say below, this causes inconsistency.
 * Benefits: More flexibility; less limitations. Less code clutter.
 * Hazards: Compiler complexity. Code performance may be less obvious.
Still, as presented here, a range of literal integers or characters is a fairly rare construct in most applications.
 /****************************************
 * 2) The "in" keyword
 ***/
 To be able to use ranges with if-else statements we must be able to test if a
 single value exists within a range. We choose the "in" keyword for this use.
 
 Example:
:if (i in 1..5) { ... }
The current syntax for this is, of course, "if ( i >= and && i <= 5 )" but a number of alternate syntaxes have been suggested. if (i <> 1:5) ... if (1 <= i => 5) ... if ( 1 : i : 5 ) ...
 Classes and structs can define the opIn method to behave like a range.
 
 * Benefits: Clearer code. More intuitive code - e.g. "foo" in Dictionary, "foo"
 in String, etc. May extend well to introspection(2.0); testing e.g. method in
 class.
 * Hazards: Compiler complexity. Parser complexity by overloading "in".
However, set membership is a relatively common idiom in applications so maybe there is something in this proposal.
 /****************************************
 * 3) Function overriding
 ***/
 The "override" attribute is normally used for making sure a function in a
 derived class really overrides a function in a base class[4]. This proposal
 extends that attribute to be usable anywhere where it makes sense.
 
 Overriding a function simply means that the overrider gets a higher priority
 than the overridee, in terms of overload resolution. With regular functions,
the
 result is that the overrider completely "shadows" the overridee.
 
 Example:
 :override void foo() { ... }  // this one has higher priority
:void foo() { ... }  // so you can't call this one
:int main()
 :{
:  foo();  // calls first
 :}
 
 Due to forward referencing it is only possible to override a function in the
 same scope *once*. However, as soon as we enter a new scope, we can do another
 override. This should be used with care though.
 
 * Benefits: More consistency/flexibility, less limitations. Allows one-line
 drop-in wrappers/replacements/modifiers to be created, targeting any scope,
thus
 easing development/debugging.
 * Hazards: Compiler complexity. Usage looks like a hack solution. Irresponsible
 use can cause complex code.
So the main usage is to 'comment out' functions for debugging purposes?
 /****************************************
 * 4) Value-based function overloading
 ***/
 This overloads a function based on the values of its parameters.
 
 Example:
:void foo(int i in 0..5) { ... }  // specialization
:void foo(int i) { ... }  // default
:int main()
 :{
:  foo(3);  // calls first
:  foo(7);  // calls second
 :}
 
 Ambiguities are illegal. That is, there can be no overlap between ranges of
 different overloads. Well, except for the defaults of course; those are special
 cases.
 
 * Benefits: Can in some cases give clearer, more maintainable code. Makes code
 more modular, by moving flow control logic to the function.
 * Hazards: Compiler complexity. Irresponsible use.
How often does one need specialization for specific values *and* calls those functions with literal parameters? This has got to be a very rare situation. Without literals, its a run-time evaluation and so compiler specialization isn't possible, and calling functions with literal parameters that can benefit from specialization is fairly rare, I would have thought.
 /****************************************
 * 5) Value-based template/mixin specialization
 ***/
 This specializes a template or mixin based on the values of the parameters.
 
 Example:
:template TFoo (T, int Size in 0..10) { ... }  // specialization
:template TFoo (T, int Size) { ... }  // default
:int main()
 :{
:  TFoo foo = new TFoo!(double, 5);  // instanciates the first one
:  TFoo foo = new TFoo!(double, 15);  // instanciates the second one
 :}
 
 The same rules apply here, no ambiguities allowed. Value-based template/mixin
 specialization is roughly the same as value-based function overloading, but
 operates on a higher level.
 
 * Benefits: Roughly the same as value-based function overloading.
 * Hazards: Same as value-based function overloading.
Any one got any real-world examples where this would be useful? -- Derek Melbourne, Australia 23/05/2005 3:24:04 PM
May 22 2005
parent Nod <Nod_member pathlink.com> writes:
In article <1ljobq2j25clf.n1f6xvyxx4s2$.dlg 40tude.net>, Derek Parnell says...
 <snip>
I have hung back on these discussion because I wasn't quite sure what was actually being put forward nor did I know what I felt about things, so far as I did recognize the proposals. Anyhow, now with this clear proposal request, I think I do fully understand it. But now I have to sit back and say "what benefit is this to D?". And I can't think of much. It seems like a whole lot of work and finessing to cater for rare situations, that can currently be taken care of in other ways.
I'm very glad that you can now join the discussion! :) As I wrote, I have omitted some technical details, and in reading your reply, perhaps too many details. There are a lot of more uses than the ones presented here, and for simplicity, i did not "tell the whole story" in really any of the proposals. More on your specific concerns below though.
[snip]

 
 /****************************************
 * 1) Generic range support
 ***/
 In D, one can use distinct values such as 42, 'c', 1.2. Sometimes though, it is
 necessary to specify a *range* of values. Partial range support is already
 supported in D through array slices. This proposal extends range support to
 anywhere where it makes sense. This includes switch statements and array
 initialization.
Now this one seems to be the most beneficial because ranges of literal values are used with some regularity. So it might be an occasional time saver.
 Example:
:switch (i)
 :{
:case 0..4: ... break;
:case 5..9: ... break;
 :default:
 :}
 
The proposal however only talks about a subset of range types. In the case of 'switch' statements, an alternate to this range could allow many more types of ranges... switch (i) { case >=0 && <= 4: ... break; case >=5 && <= 9: ... break; case >9 && < 20: ... break; case 20, 21, 24, 29: ... break; case >= 22 && !(24,29): ... break; case <50: break; default: }
Your example, converted: :switch (i) :{ :case 0..4: ... break; :case 5..9: ... break; :case 10..19: ... break; // exclusive ranges can be rewritten as inclusive :case [20, 21, 24, 29]: ... break; // arrays are allowed as ranges :case 22..int.max & ![24, 29]: ... break; // bitwise operators allowed :case int.min..49: break; :default: :} According to the full proposal, these are all valid. However, that entire switch statement is not valid, since ranges must be non-ambiguous, i.e not overlap.
A 'range' could even be made similar to enums. That is, we could have named
ranges to assist readibilty.

  range int patA = {>=0 && <=4};
  . . .
  switch(i) { case patA: . . .}
Yes, I would like this, but I am concerned with compiler complexity if we decide on full named literal support. One can successfully emulate that behaviour using a class or a struct by overriding opIn.
 Example:
:char[] = 'a'..'e';  // "abcde"
Doesn't this assume a collating sequence based on ASCII. What do you do with non-ASCII characters?
Hmm, good question. I guess one would have to specify the character set in some way. This could be a simple array from 0..255. Unicode would have to use a class.
 Ranges does not have to be solid, they can have holes in them. It is possible
to
 use the bitwise operators to join &, subtract |, take difference ^ or negate !
 ranges. 
How would you code holes? 0 .. 10 except 6?
0..10 & ![6]
Range literals are all inclusive[3].
As you say below, this causes inconsistency.
 * Benefits: More flexibility; less limitations. Less code clutter.
 * Hazards: Compiler complexity. Code performance may be less obvious.
Still, as presented here, a range of literal integers or characters is a fairly rare construct in most applications.
As you note yourself below, literals is not the only use.
 /****************************************
 * 2) The "in" keyword
 ***/
 To be able to use ranges with if-else statements we must be able to test if a
 single value exists within a range. We choose the "in" keyword for this use.
 
 Example:
:if (i in 1..5) { ... }
The current syntax for this is, of course, "if ( i >= and && i <= 5 )" but a number of alternate syntaxes have been suggested. if (i <> 1:5) ... if (1 <= i => 5) ... if ( 1 : i : 5 ) ...
Yes, but none are as good as mine! :) Seriously though, that's just syntax, if one has clear benefits versus my proposal, I can consider changing it.
 Classes and structs can define the opIn method to behave like a range.
 
 * Benefits: Clearer code. More intuitive code - e.g. "foo" in Dictionary, "foo"
 in String, etc. May extend well to introspection(2.0); testing e.g. method in
 class.
 * Hazards: Compiler complexity. Parser complexity by overloading "in".
However, set membership is a relatively common idiom in applications so maybe there is something in this proposal.
Yup.
 /****************************************
 * 3) Function overriding
 ***/
 The "override" attribute is normally used for making sure a function in a
 derived class really overrides a function in a base class[4]. This proposal
 extends that attribute to be usable anywhere where it makes sense.
 
 Overriding a function simply means that the overrider gets a higher priority
 than the overridee, in terms of overload resolution. With regular functions,
the
 result is that the overrider completely "shadows" the overridee.
 
 Example:
 :override void foo() { ... }  // this one has higher priority
:void foo() { ... }  // so you can't call this one
:int main()
 :{
:  foo();  // calls first
 :}
 
 Due to forward referencing it is only possible to override a function in the
 same scope *once*. However, as soon as we enter a new scope, we can do another
 override. This should be used with care though.
 
 * Benefits: More consistency/flexibility, less limitations. Allows one-line
 drop-in wrappers/replacements/modifiers to be created, targeting any scope,
thus
 easing development/debugging.
 * Hazards: Compiler complexity. Usage looks like a hack solution. Irresponsible
 use can cause complex code.
So the main usage is to 'comment out' functions for debugging purposes?
No, its main use can be found together with value-based function overloading, for overriding specific values in specific scopes. I agree that this proposal wouldn't and *shouldn't* be used very often, but it is very powerful in that it allows you to modify the behaviour of any function in *only a specific scope* and *without modifying other code*. In big projects, this is currently something that's neigh impossible to do without touching a lot of code. In short: in the rare cases when you *do* need to use it, it's a real time-saver. It's not like it's a huge change of the language anyways :)
 /****************************************
 * 4) Value-based function overloading
 ***/
 This overloads a function based on the values of its parameters.
 
 Example:
:void foo(int i in 0..5) { ... }  // specialization
:void foo(int i) { ... }  // default
:int main()
 :{
:  foo(3);  // calls first
:  foo(7);  // calls second
 :}
 
 Ambiguities are illegal. That is, there can be no overlap between ranges of
 different overloads. Well, except for the defaults of course; those are special
 cases.
 
 * Benefits: Can in some cases give clearer, more maintainable code. Makes code
 more modular, by moving flow control logic to the function.
 * Hazards: Compiler complexity. Irresponsible use.
How often does one need specialization for specific values *and* calls those functions with literal parameters? This has got to be a very rare situation. Without literals, its a run-time evaluation and so compiler specialization isn't possible, and calling functions with literal parameters that can benefit from specialization is fairly rare, I would have thought.
Without literals, it will indeed be rewritten as an if-else or switch-case statement by the compiler, and evaluated at run-time.
 /****************************************
 * 5) Value-based template/mixin specialization
 ***/
 This specializes a template or mixin based on the values of the parameters.
 
 Example:
:template TFoo (T, int Size in 0..10) { ... }  // specialization
:template TFoo (T, int Size) { ... }  // default
:int main()
 :{
:  TFoo foo = new TFoo!(double, 5);  // instanciates the first one
:  TFoo foo = new TFoo!(double, 15);  // instanciates the second one
 :}
 
 The same rules apply here, no ambiguities allowed. Value-based template/mixin
 specialization is roughly the same as value-based function overloading, but
 operates on a higher level.
 
 * Benefits: Roughly the same as value-based function overloading.
 * Hazards: Same as value-based function overloading.
Any one got any real-world examples where this would be useful?
Heh, not off the top of my head, but then, I'm rather tired :) I seem to remember writing one in draft 3. You might want to check that one out. If anyone else could figure something out it would be cool. And remember, one doesn't have to use ranges of ints, one can use chars, floats, arrays of strings, arrays of other stuff, custom classes like Regex, Dictionary, Tree... Of course, anything that's not possible to evaluate at compile-time will cause *all* specializations to be instantiated, and the correct one will be chosen at run-time.
-- 
Derek
Melbourne, Australia
23/05/2005 3:24:04 PM
As said, nice to see more people joining the debate. Keep 'em coming folks! -Nod-
May 23 2005