Method overloading
BlitzMax Forums/BlitzMax Programming/Method overloading
| ||
Whilst I'm working on implementing it, what are peoples views on having Method overloading? Here's a small example of overloading: SuperStrict Framework brl.standardio Local rect:TRect = New TRect Local draw:TDraw = New TDraw draw.DrawRect(1, 2, 3, 4) draw.DrawRect(rect) Type TRect Field x:Int Field y:Int Field w:Int Field h:Int End Type Type TDraw Method DrawRect(x:Int, y:Int, w:Int, h:Int) Print "Drawing with x, y, w, h" End Method Method DrawRect(rect:TRect) Print "Drawing with rect" End Method End Type The above code would produce the following output: Drawing with x, y, w, h Drawing with rect Normally in BlitzMax you would need to give a different name to one of the DrawRect() methods. In some cases, you may end up with several methods with similar names performing the same functionality from different argument types. With overloading, you can simplify your APIs. Method overloading also opens up the potential to add operator overloading... |
| ||
Does this include constructors? If there's no performance hit, i don't see it as a bad thing. Although it will break Blide, I suppose its the price of progress. |
| ||
OMG that would be fantastic! I'd be SUPER THRILLED! |
| ||
Does this include constructors? I don't see why not. Perhaps something along the lines of : Local rect:TRect = New TRect(10, 10, 100, 100) Type TRect Field x:Int Field y:Int Field w:Int Field h:Int Method New(x:Int, y:Int, w:Int, h:Int) Self.x = x Self.y = y Self.w = w Self.h = h End Method End Type |
| ||
At first I thought no, you can't have same method name in one type, but after doing some research I think why not. So it's cool. EDIT: Of course it would give an error if there were 2 methods with a same name and same parameter types. -Henri |
| ||
@Henri Re edit. Why? Are you saying I can not override earlier methods? |
| ||
Nope. I'm saying this should not be allowed: Type Test Method Do(this:Int) Print this EndMethod Method Do(that:Int) Print that EndMethod EndType -Henri |
| ||
Hahaha, that's pretty funny, Henri! xD I would almost allow such a silly stuff to "compile", just to mess with the person, who wants it! xD Jokes aside, Function overloading should be in there, too, for consistency's sake. A complete dream come true would be operator overloading! With my vector classes my life would be infinitely more enjoyable to have that!!! 8D |
| ||
While your on Brucey, and I know its only marzipan, but:-Type TRect Field x:Int Field y:Int Method Init(newX:Int,newY:Int) Self.x = newX Self.y = newY End Method 'Overload + operator so we can add two types together Method Operator<+>:TRect(rect:TRect) Local tempRect:TRect = New TRect tempRect.x = Self.x + rect.x tempRect.y = Self.y + rect.y Return tempRect End Method EndType Local rect1:TRect = New TRect rect1.Init(10,20) Local rect2:TRect = New TRect rect2.Init(100,200) 'Use the overloaded + operator for our types Local rect3:TRect = rect1 + rect2 Print rect3.x 'Prints 110 Print rect3.y 'Prints 220 Cheers! :) Dabz |
| ||
Method Operator<+>:TRect(rect:TRect) Yeah, I'm not sure about the syntax for that yet. But while we're on the topic, do you want to make a list of all the operators under consideration? ;-) |
| ||
Well, arithmetic operators, modifying assignments operators(if pos)... I only generally use arithmetic ones and compound ones in C++, as I like the 'object = object + anotherObject' form of writing it over the more usual 'object = AddObjects(object, anotherObject)' setup! Like I said, its marzipan really! Dabz |
| ||
To voice an opinion way too late to make a difference, you should consider making operator polymorphism available before operator overloading. i.e.:rect1 + rect2 ' is identical to, and in fact just syntax sugar for: rect1.__plus__(rect2) ...which would otherwise follow the existing runtime-polymorphic rules. Special-casing operators to go straight to overloading will result in a multi-tier language design with muddy semantics. There's also very little practical value to demanding operators be static while methods be dynamic without also introducing templates (i.e. turning your language into C++). And nobody sane wants that. Personally I don't think overloading is a good idea, mainly because I don't buy the reasoning that it does simplify an API to have the same name refer to multiple things without any control over this on the language side (there's no easy way for the compiler to enforce that the two DrawRect methods are semantically related, meaning you open up more error-surface by allowing overloading). If you implement generics (true generics, don't waste your time with templates), you can completely replace overloading with Dictpassing anyway, allowing for all of the same advantages of shorter/clearer code (i.e. two methods both named DrawRect) without any overloading machinery or linguistic complexity. (In general, generics - parametric polymorphism - make a language simpler, while overloading - ad-hoc polymorphism - makes it massively more complex.) Dictpassing works best with an "open" or "using" directive to allow easy access to scoped names. Given such a directive, a dictpassing version of BlitzMax could hypothetically look like this: Notice that you trade off making your call externally explicit with the fact that a) both of the implementations of MultiplyAndAdd still get to use short names for operators that handle completely unrelated types; and b) a guarantee that all implementations of MultiplyAndAdd have exactly the same underlying structure (i.e. even though Int and Vec3 are unrelated and their +/* do different things, the enclosing function does the same thing in the context of what it means for that type). |
| ||
First thought: great! Especially if this also implies the constructor/New Method. But a concern of mine would be: will there be an IDE picking this up... I admit being spoiled by Visual Studio, but for me, it's too hard to go back to non IDE supported classes/methods. |
| ||
I'd see it as a big plus! Brucey you just keep making Max better and better. |
| ||
Henry: I'm saying this should not be allowed: Type Test Method Do(this:Int) Print this EndMethod Method Do(that:Int) Print that EndMethod EndType How should the system know what kind of "Do()" you are using when passing an "int" ? Overloading needs to get different amount of params passed, or different types. What you are attempting could be done by using a different "Do()" in an extending type (a extends b). bye Ron |
| ||
Type Test Method Do(this:Int) Print this EndMethod Method Do(that:Int) Print that EndMethod EndType No, that should indeed not be allowed. C# raises two helpful errors: Error: Type 'Test' already defines a member called 'Do' with the same parameter types Error : The call is ambiguous between the following methods or properties: 'Test.Do(int)' and 'Test.Do(int)' |
| ||
bruce. is this publicly testable ;) |
| ||
Let the man do his thing, Adam! We don't want him to get boxed in while setting it all up! ;) |
| ||
is this publicly testable ? There's a "bcc_overload" branch in github ( https://github.com/bmx-ng/bcc/tree/bcc_overload ) with the initial work committed, which currently clean-builds against the latest NG brl and pub modules. Doesn't yet support function/New() overloading, but it should work enough for having a play with. I'd suggest creating a new "BlitzMax area" for testing this, as it uses method name mangling which is incompatible with the current bcc (or of course you can simply swap between the new and old bcc's and rebuild everything each time). |
| ||
ok, no problem. I'll slowly get it all ready :) |
| ||
ar: creating /BlitzMaxNG_overload/mod/brl.mod/blitz.mod/blitz.debug.linux.x86.a Compile Error: Unable to find overload for bbgcvalidate(Int). Argument #1 is "Int" but declaration is "Byte Ptr". [/BlitzMaxNG_overload/mod/brl.mod/appstub.mod/debugger_mt.stdio.bmx;627;0] (EDIT: removed second error, as it was a userbased error - aka based on my faults :-)) Sample I wanted to compile: bye Ron |
| ||
Your first error is fixed in github. The other one I cannot reproduce, either with my local copy or a clean clone from the repo :-) Your example outputs: Constructing machine: Machine Constructing terminator: Terminator :o) |
| ||
The other error was based on a borked bcc.conf (pointing to another BlitzMax installation) ... Question: If I append the following to the sample above: 'should print: Constructing machine: Machine" ? factory.Construct(TMachine(New TTerminator)) I pass the variable as "TMachine" - so should it still print "Terminator" (so use the "instance's type") or should it output "Machine" (use the "passed type")? bye Ron |
| ||
The cast to TMachine will cause it to choose Construct(obj:TMachine). It will print "Terminator" because the object is an instance of TTerminator. Therefore the output in the third case would be : Constructing machine: Terminator |
| ||
Ok, so it chooses by the "given" type, not the "original" type. Seems to do what it should. Let's see with what others come up. PS: recognized that my question wasn't that correct at all: should have asked what "construct()" is called then. Glad you answered it already. bye Ron |
| ||
Here's an example of constructor overloading from the latest commit:SuperStrict Framework brl.standardio Local b:TBase = New TBase Print b.ToString() b = New TBase(20) Print b.ToString() Local s:TSub = New TSub Print s.ToString() s = New TSub(60, 50, 10) Print s.ToString() Type TBase Field a:Int Method New() a = 99 Print "~nTBase New()" End Method Method New(a:Int) Self.a = a Print "~nTBase New(int)" End Method Method ToString:String() Return " TBase = " + a End Method End Type Type TSub Extends TBase Field b:Int Field c:Int Method New(a:Int, b:Int, c:Int) Self.a = a Self.b = b Self.c = c Print "TSub New(int, int, int)" End Method Method ToString:String() Return " TSub = " + a + ", " + b + ", " + c End Method End Type which outputs the following: TBase New() TBase = 99 TBase New(int) TBase = 20 TBase New() TSub = 99, 0, 0 TBase New() TSub New(int, int, int) TSub = 60, 50, 10 Still a ways to go, but testing is going well so far. |
| ||
Without testing: How would "TSub" call the "New"-function of TBase. Is this even possible / intended? I mean, when using "Method Init:TTypeName()" you are able to override this in an extended type and then call "super.Init()" to also handle the stuff done there. possibilities: Method New(a:int) super.New() End Method Method New(a:int) 'parental "new" are already called - what happens to overloaded ones? End Method bye Ron |
| ||
New() already calls its parents New() automatically. This would simply be extended to overloaded New() as well, it would call its parents equivalent or default New() if none is defined. This is how most other OO languages do it. Another thing is if you want to allow calling a New() in the same type. Type Base Method New() ' lots of work EndMethod Method New(x:Int) New() ' .. little work EndMethod EndType To follow the semantics of New() it would be required to be the first call in the method. |
| ||
New() already calls its parents New() automatically. So the question then is: which one is called? a: New() a: New(i:int, j:int = 0) a: New(i:int) b extends a b: New(i:int) With "b" wanting to take over the "initialization" of a, which "new()" is called then. And how to call a specific "super.New()" ? Instead of "New()" (which does not state another scope than the "local" one) I would really prefer ("Super.New()") as "super" is already the keyword to access the parental class definition. bye Ron |
| ||
So the question then is: which one is called? The one with the same prototype, just like with methods. Unless it hasent been overridden in which case vanilla New().Yeah, if your gonna allow calling constructors of base classes, "Super.New()" is the only way really. What i meant with my sample above is to call another constructor within the same type. To limit the copy-pasta. |
| ||
I agree with Derron. An extended class should have to explicitly call its parent constructor. Automatically calling a parents constructor could produce unwanted effects. Using Super.New(argument[s]) makes the most sense to me. |
| ||
This would also allow to use other New()-variants then the one called for the extended type. back to my sample: Type B extends A Method New(i:int) Super.New(i, 2) 'calling New(i:int, j:int = 0) of a 'do something else End Method End Type Still in question: how does the compiler know that New(i:int) is meant instead of New(i:int, j:int = 0) when calling Super.New(10). Isn't this possible at all as the New(i,j=0) variant isn't 100% distinguishable? -> for overloaded methods "default params" are not allowed - or are they? bye Ron |
| ||
Id rather not have to explicitly call Super.New() in every New(). Its already automatically called, why change the semantics? There is no ambiguity here. Having the option of also calling Super.New() yourself is a good thing too, at which point the automatic calling is not done. For default arguments, the same can be said of regular overloaded methods. Pascal for example, treats this as an error. If you are to allow such ambiguity, i would go with calling "New(x)" because of the number of arguments. You gave it 1 argument, so you get the 1 argument version. Unless there only is the 2 argument version then that is what you get instead. |
| ||
Grable: After re-analyzing Brucey's example I see what you are referring to. I had it my head a New method with parameters passed to it (Example: New(int, float, int)) in which I would want to have to explicitly call from an extended object, but, yes, a default New constructor (without parameters) should automatically be called. In short, I'm with you :) |
| ||
On the one hand: Do not do "default New()" is called "parametrized New()" is not called. This leads to "is super.new() called now?" thoughts which means: ambiguity. On the other hand: If "New(params)"-definition is the same for base and extended type, then the "super.New(params)" should be called - at least is this what I would expect to happen (because of "super.new()" being called implicitely in vanilla BMX) . But: vanilla BMX does not contain overload, so expectations to how things behave are non-existent. As I expect "MyMethod(int)" to not call "Super.MyMethod(int)" on its own, I am not sure why I should expect "New()" to behave similar. All in all even in vanilla this isn't consequently done then. How do other languages tackle it? bye Ron |
| ||
@seriouslee hehe, dont worry. Its all up to Brucey in the end anyway ;) Id also like to know the semantics of the below: Type A Method New( x:Int) EndType Type B Extends A Method New( x:Int, y:Int) EndType Local t:A = New A ' allowed? Local b:B = New B(1) ' allowed? |
| ||
Id also like to know the semantics of the below t calls the default constructor for A b calls the overloaded constructor for A which is what you'd expect, I think? |
| ||
vanilla BMX does not contain overload, so expectations to how things behave are non-existent Only if you've never used other programming languages, I suppose? |
| ||
a: New(i:int, j:int = 0) a: New(i:int) Probably not a good idea, since you would not be able to call New a(i). I suppose that could be considered applicable for a compilation error - since you are introducing ambiguity within a single type. On the other hand, a: New(i:int, j:int = 0) b: New(i:int) is fine, as the scope separates ambiguity here. If you call new b(i), it will call b's constructor - since the scope takes precedence. |
| ||
I hadn't considered constructor chaining... I suppose it is a reasonable thing to implement, as with Super.New() calling, all of which would need to be done as the first statement. I'll look into that. It gets somewhat complicated, I see ;-) |
| ||
Only if you've never used other programming languages, I suppose? So BMX-NG is only for people being used to other programming languages? When it comes to "oop" I never really used overloading (only a bit tinkering in java, but I avoided such "odd situations" - and "overloaded constructors"). What I wanted to express in the prior posts is: there should be some "rules" a coder could follow. "Method New()" is already breaking it (you would better have named it "__construct" or so - to _not_ name it like the keyword. keyword type new bla -> (new instance of bla).__construct() delete bla -> bla.__delete() Because now it seems as if methods could be called that way ("methodname type"). Of course this inconsistency is not your (bmx-ng's) fault but blitzmax'. Again: do not set your expectations to be "what all others expect" - there is a reason to have me and other "amateurs" on your side: we tell what we expect, without the "genious knowledge" some of the more experienced coders have. But also do not get that wrong, it is your decision how things will evolve or not. We are surely able to bend our minds accordingly until we get used to it. Just want to "brainstorm" a bit while things are not settled yet. bye Ron |
| ||
which is what you'd expect, I think? Yeah, i expected as much on account of bmx default constructors (even if they are empty :p).Though there are languages that do not have default constructors (like Pascal and C++) in which an object must use one of the defined ones. I dont know which is better really. Objects in blitzmax always have zeroed out data, so they are technically initialized even without a constructor. So not requiring to call a constructor does fit with prior conventions in regards to constructor functions/methods doing the initializing in vanilla bmx. The only difference would be that you cant initialize an already created instance, unless calling New() on them would be allowed ;) |
| ||
Objects in blitzmax always have zeroed out data They contain the "null"-equivalents. so int = 0 string = "" object = null For me this means, they are _not_ initialized. Exception is if your type already predefines things: type test field listA:TList = CreateList() ' not null on "new test" field listB:TList ' null on "new test" end type bye Ron |
| ||
I guess it depends on where your coming from. In C initialized==0 or any other valid pointer/data, as everything not explicitly initialized is random garbage. Which is why i used "technically" ;) The main point is that they are in a defined state. Still, whether to allow default constructors when there are other defined ones is a hard one. As doing so might be seen to create uninitialized objects with no hope of ever being initialized to the proper state. Stay with convention or go for more correctness? :p |
| ||
Got and understood that point about null/initialized. (albeit I am not sure but think to remember that I also had "garbage" values in some field-properties - years ago - maybe that was changed some versions ago or so?) @ New Wouldn't it be easier to disallow that feature at all (if we would not come to a consense :-)) there is still "new bla.init()" (which could be overloaded without trouble then). But like said: we will get used to whatever you will come up. Of course "SendMessage()" and other built-in methods should be overloadable like normal methods. bye Ron |
| ||
In BlitzMax, all fields are initialised to *something*, either their default "zero" state, or whatever was specified in the field declaration. @ New Well, GW wanted it, so I thought I may as well implement it :-p The general direction I'm taking with the overloading is to work in a similar way to Java, whose rules are basic and fairly easy to implement - but of course, in the flavour of BlitzMax. Whether or not I can get it right, is another matter entirely :-) |
| ||
Overloaded constructors is the probably the only kind I would use. If it gets into swampy territory, then don't add it on my account. Backwards compatibility is what would effect me the most in my projects. |
| ||
Backwards compatibility is what would effect me the most in my projects. I also think this should be one of the most important factors, even for new users not having to port any code (code archives etc.) |
| ||
If you did not use overloading in your current projects (chances are high if you use vanilla BMX ...) backwards compatibility should be no issue. Once you use overloading then, it of course wont work with vanilla BMX any longer. (If you wrote it in a way, vanilla ignored the new "functionality", at least your application logic would be broken). bye Ron |
| ||
If you did not use overloading in your current projects (chances are high if you use vanilla BMX ...) backwards compatibility should be no issue. Well, it does once we start renaming New() to __construct() or similar changes... And while I've grown fond of using PHP over the years, naming conventions should probably be copied from elsewhere ;) |
| ||
The name New() isn't changing. |
| ||
Here's a small contrived example which shows constructor chaining :SuperStrict Framework brl.standardio Local s:TSub = New TSub(15) Print s.i Type TBase Field i:Int Method New() New(10) Print "TBase() : " + Self.i End Method Method New(i:Int) Print "TBase(int) : " + Self.i Self.i = i End Method End Type Type TSub Extends TBase Method New() Print "TSub()" End Method Method New(i:Int) Print "TSub(int) : " + Self.i Self.i = i End Method End Type which outputs the following: TBase(int) : 0 TBase() : 10 TSub(int) : 10 15 And now a description of what occurred :-) Initially, TSub.New(i:Int) is called. Since it doesn't call another constructor itself, the default is to call the Super type default constructor. So, TBase.New() is called next. This in turn calls TBase.New(i:Int). Finally, the Object default constructor is called. :o) |
| ||
Thanks for the info, Brucey. Question: Is New() required? Would the following be acceptable?Local a:TType = New TType(15) Type TType Field value:int Method New(newValue:int) value = newValue End Method End Type |
| ||
Hi, your example contains New() ? EDIT: Oh, I thought you meant omitting New() totally. -Henri |
| ||
Is New() required? No. You don't need to override the default New constructor. In your example, you can still call "New TType" to create a new instance of TType... SuperStrict Framework brl.standardio Local a:TType = New TType(15) Print a.value a = New TType Print a.value Type TType Field value:int Method New(newValue:int) value = newValue End Method End Type .. would output : 15 0 |
| ||
Since it doesn't call another constructor itself, the default is to call the Super type default constructor. So if adding a "New()" to the overloaded New(int) of TSub would then call this instead of "Super.New()" ? Also: "super.New()" is always called - except there is no parent? So for a "base class" all "New(...)"-variants wont call New() too? Edit: feel free to update the github repo - so I could answer above's questions on my own. bye Ron |
| ||
So if adding a "New()" to the overloaded New(int) of TSub would then call this instead of "Super.New()" ? If your code looks like this : Type TSub Extends TBase Method New(i:Int) New() Print "TSub(int) : " + Self.i Self.i = i End Method End Type It would call New() in TBase (either the default or the overridden one). There is *always* at least a default New(). If your code looks like this : Type TSub Extends TBase Method New() Print "TSub() : " + Self.i End Method Method New(i:Int) New() Print "TSub(int) : " + Self.i Self.i = i End Method End Type calling New TSub(10) would result in a call to TSub New(). |
| ||
Tried the newest revision - works nicely. outputs: TBase.New() ---- TBase.New(i:int) ---- TBase.New() TSub.New() ---- TBase.New(i:int) TSub.New(i:int) ---- TBase.New() TOtherSub.New() ---- TBase.New() TOtherSub.New(i:int) ---- So what is happening: - if you extend a type the New-Methods will call the parental New()-method - they call the non-overloaded-New()-method (without params) - if you call a custom "Super.New(xyz)"-method, the default Super.New() is skipped Is this the "expected" behaviour? or should it try to call the same overloaded New(xyz)-method of its parent (if available) instead? In this case this should mean, that the last output lines would look like this: TBase.New(i:int) '<---- with param now TOtherSub.New(i:int) Like said: I think I would go d'accord with both solutions, just asking what is "expected" there (by the other Blitzmax-users - interested in NG). bye Ron |
| ||
Is this the "expected" behaviour? Yes. Whenever you don't specifically call a Super constructor in New() it automatically calls the default super constructor (i.e. the one with no parameters). This is also what happens with the legacy compiler. |
| ||
Here's an example of a simple copy constructor :SuperStrict Framework brl.standardio Local rect:TRect = New TRect(10, 10, 100, 200) Print rect.ToString() Local r2:TRect = New TRect(rect) Print rect.ToString() Type TRect Field x:Int Field y:Int Field w:Int Field h:Int Method New(x:Int, y:Int, w:Int, h:Int) Self.x = x Self.y = y Self.w = w Self.h = h End Method Method New(rect:TRect) ' copy constructor ;-) New(rect.x, rect.y, rect.w, rect.h) End Method Method ToString:String() Return x + ", " + y + ", " + w + ", " + h End Method End Type :o) |
| ||
This is also what happens with the legacy compiler. This is a matter of interpretation. For _me_ it calls the super.method of the one I am in at this moment. So when in new(int) it should call super.new(int). Your interpretation is: when you create a new object in vanilla bmx ...all new() methods "downwards" are getting called. As new(int) is an equal creator to new() i do not see the need to make new() a preferred method. Backwards compatibility isn't given in both scenarios. But like said..just a personal thought ... no need to adjust the behaviour if I am alone with that :-) bye Ron |
| ||
So far, the following appears to be working : * Method overloading. * Function/static overloading (in types). * Constructor (New) overloading. * ToString, Compare and SendMessage overloading. which probably covers most things. The system uses name mangling when generating source. You can optionally disable mangling of Functions using { nomangle } metadata - useful for publicly accessed type functions (often used when calling into BlitzMax from C/C++). |
| ||
Cool! Will this be merged into the main branch? |
| ||
Yes, at some point. I'm tidying up my modules in the meantime, applying nomangle where appropriate - which "generally" is all that needs to be done. There are the occasional edge cases where a method argument will need to be specifically cast to a type. For example, if you are passing a Double arg into a Float param you will need to manually cast it down to Float, or you will get a compilation error which should tell you that you can't pass a Double into a Float. Here's an example of what I mean : SuperStrict Framework brl.standardio Local t:TType = New TType Local a:Int = 10 Local b:Int = 5 Print t.add(a, b) ' ok. widens int to float Local c:Float = 6.8 Local d:Float = 9.1 Print t.add(c, d) ' ok. args match param types Local e:Double = 20 Local f:Double = 6 'Print t.add(e, f) ' compile error. Cannot narrow double to float. Print t.add(Float(e), Float(f)) ' ok. cast down to float. Type TType Method add:Float(a:Float, b:Float) Return a + b End Method End Type |