Generics
BlitzMax Forums/BlitzMax NG/Generics
| ||
I *really* want to get generics into NG at some point. But it's a tad complex and I need to write something down and see if anyone else has some comstructive feedback with regards an implementation of it. (I realise that's probably a very small audience, but I welcome any feedback much the same) Looking at the Monkey implementation it feels way overly complex - in that it appears to generate special type/instance specific classes for each type you use in place of the generic one. Maybe I'm missing some point or other, but surely we can accomplish all the generics handling/validation at the compile phase and not build instance specific types at all? Here's how I see things. This is a basic generic type : Type TContainer<T> Field _t:T Method get:T() Return _t End Method Method set(_t:T) Self._t = _t End Method End Type "T" basically represents some Type of Object. Let's say we want it to contain a String... Local stringContainer:TContainer<String> = New TContainer<String> In a code generation sense, I don't see any need to generate a "special" type for this instance. In essence "T" is an Object, so the field _t is basically _t:Object. The magic lies in how the compiler (bcc) understands that the variable "stringContainer" will only accept a String into its set() method. stringContainer.set("Hello World") is valid. Local myObj:TSomeType = New TSomeType stringContainer.set(myObj) should throw a compiler error, because myObj is not a String. I can't see any immediate problems with that... You should also be able to construct things like this: Interface IPair<K, V> Method GetKey:K() Method GetValue:V() End Interface Type TPair<K, V> Implements IPair<K, V> Field key:K Field value:V Method New(key:K, value:V) self.key = key self.value = value End Method ... etc In this case again, the generated code will declare key and value fields as Object. There is also something called a bounded type parameter, this is where "T" must be of a specific Type (or interface). So, for example : Type TWarrior<T Extends TWeapon> Field weapon:T Method New(weapon:T) self.weapon = weapon End Method Method attack() weapon.attack() End Method ... etc In this case, T must be of type TWeapon when an instance of TWarrior is created... Local archer:TWarrior<TBow> = new TWarrior<TBow>(New TLongBow) archer.attack() The generated code for TWarrior would make the field weapon, a TWeapon, allowing to contain any subclasses of TWeapon. Again, all the work is done by the compiler to ensure type safety. Are there any obvious flaws here? I don't believe at runtime an instance of TWarrior needs to know that its instance is specifically of TBow. Technical discussion welcome :-) |
| ||
This falls short with Basic types though, but for the objects it reminds me of Type Erasure. What Java does, which has its share of problems... Personally i like the monkey way of just replacing types and generating more code, even if it can balloon the code size. Though this can be reduced somewhat by only generating actually used types. Probably a hybrid approach would work best here? |
| ||
My initial thought is that as long as the compiler preserves current compatibility with vanilla bmax. it sounds good. As far as the warrior example, don't we have that already? Type tWeapon Abstract Method attack() Abstract End Type Type tLongBow Extends tWeapon Method attack() Print("thwang..") End Method End Type Type tWarrior Field wep:tWeapon Method New(w:tWeapon) wep = w End Method End Type Local soldier:tWarrior = New tWarrior(New tlongbow) |
| ||
Are there any obvious flaws here? I don't believe at runtime an instance of TWarrior needs to know that its instance is specifically of TBow. Yes. Having generics in BlitzMax would be awesome, but doing type erasure like this puts some severe limitations on the ways these generics can be used. The biggest one is that it completely excludes all non-object types from being bound to type parameters. For example, the #1 reason why I find collections such as TList in BlitzMax annoying to use is because having a list of, say, Int, is simply not possible without either A) wrapping every Int in another object, which comes with a performance hit, or B) writing a separate version of the class such as "TIntList", which would basically just be the original class with the element type replaced by Int. This is already annoying in Java (which does workaround A by having separate dedicated wrapper classes for every primitive type), but it would be even worse in BlitzMax, where non-object types don't only amount to primitives, but also to function references, pointers and structs, in other words an infinite number of types that have no inheritance relationships between each other. This wouldn't be a problem if TList<T> was a truly generic type, with no constraints as to what T can be. In addition, type erasure as it is done in Java also creates a bunch of other limitations. For instance, creating a new object from a type parameter or casting to a generic type is not possible without knowing the exact type at runtime. More detailed descriptions on this can be found here and here. Implementing "true" generics without these restrictions would doubtlessly be more difficult, but they would be much more powerful and useful too. |
| ||
Brucey, it would be nice for NG to have the same as how Blitz3D handled types in a list, not as a replacement (which the current seems really confusing) but as an option. That way it would be much easier to port existing bb code to blitzmax. I know the code converter does this already but it's not 100% accurate and most of the time breaks when you got some complex code to convert. Maybe instead of this Generics thing which looks like it will complicate NG more, add more BB like features if possible? I can see B3D/Plus users will have an easy transition to Max as this is the one of the hardest part to manually do in Max. |
| ||
Looking at the Monkey implementation it feels way overly complex - in that it appears to generate special type/instance specific classes for each type you use in place of the generic one Generics is syntatic sugar so I think I'd expect something along those lines would happen. I suppose you could solve the problem with method overloading with the correct parameter types but then I think the need for building complete types with a generic instance is to solve generics using generics. For eg how do you semant AContainer without building the complete type for CompleteList ( typeof TList<Something> )? Type Something EndType Type SomethingElse Endtype Type List<T> EndType Type Container<T> EndType Local CompleteList:TList<Something> = New List<Something> Local AContainer:Container<CompleteList> = new Container<CompleteList> Local BContainer:Container<SomethingElse> = new Container<SomethingElse> |
| ||
I'm pasting the given example for reference. If you can look at Blitz3D's way of managing types and list it's way dead simple you can oversee and understand what is going on..; Define the CHAIR Type Type CHAIR Field X Field Y Field HEIGHT End Type ; Create 100 new chairs using FOR ... NEXT using the collection name of ROOM For tempx = 1 to 10 For tempy = 1 to 10 room.chair = New Chair room\x = tempx room\y = tempy room\height = Rnd(0,10) ; set a random height 0 to 10 Next Next ; Move them all over 1 (like the description example) For room.chair = Each chair room\x = room\x + 1 Next I know Blitzmax is flexible enough, so at least have the option to do it like the B3D way? This was not available in vanilla, I was hoping it can make its way to NG. Saw a few topics discussing about beauty in code. Well I don't believe on that, but making it easy on the coder would be a good thing times a 100. The reason Blitzmax is a success over Monkey is it's in between B3D and Monkey in regards to complexity, in my opinion. I hope it does not turn out to be another Monkey fail. |
| ||
it's way dead simple It doesn't look dead simple.... maybe you've left out some important information, like what "room" is, exactly. I'm assuming that it automagically adds the new Chair to a hidden array, and "room" just references the last one in the array? I don't think it is more clear than what you can do in BlitzMax. @generics Okay, so something like this would be better? Interface ICollection<T> Method Add:Int(e:T) Method ToArray:T[]() End Interface Type TArrayList<T> Implements ICollection<T> Field size:int Field array:T[0] Method New(initialCapacity:int = 16) array = new T[initialCapacity] end method Method Add:Int(e:T) if size >= array.length then ' expand array array = array[..size + size *.3] end if array[size] = e size :+ 1 return True End Method Method ToArray:T[]() return array End Method End Type Local intArray:TArrayList<int> = new TArrayList<int> intArray.Add(10) intArray.Add(128) Local stringArray:TArrayList<String> = new TArrayList<String> stringArray.Add("Hello") stringArray.Add("World") ... where you can just use whatever type/primitive you want? I suppose we wouldn't generate any code for the generic types, but each "instance" would require the "generic template" to be fully generated. So, by creating a TArrayList<int> object, a pure int-based TArrayList could be generated. bcc could keep track of what types of TArrayList had already been used, so we'd only be generating one TArrayList<int> implementation for the app/module. It seems doable. |
| ||
RustyKristi: Types in BlitzBasic are simple method-less classes with a built-in linked list. In BlitzMax, you can easily emulate that by using a Type and a TList. For example, put the list inside your type, add every newly created object to the list, and remove it again when you don't need it anymore. Maybe add a few more convenience methods to replace Before, After, etc. - and you've got what BlitzBasic had. That's pretty much exactly what the converter does, it just has some problems translating the BlitzBasic code correctly. But if you want to do things the exact same way as in BlitzBasic, why not just use BlitzBasic? BlitzMax is a different language, so of course it has differences. There is absolutely no reason to try and put an awkward, limited feature from an outdated programming language into a much newer and better one, where it has been superseded by way more powerful tools. This might sound a bit rude, but if you consider something like that complicated or confusing, you might not have much use most of BlitzMax's other features anyway. The code implies that you seem to have a slight misconception of how types work in BlitzBasic as well. You have a type named CHAIR, and then you're creating 100 of these chairs in a loop, but assigning each of them to a variable named...room? A room and a chair are very different things. A room might contain chairs, but what this variable of yours actually stores is one individual chair at a time. By doing For room.chair = Each chair, for example you're assigning every existing chair to the variable "room", one after another, and room\x = room\x + 1 increases the variable "x" of that specific chair by 1. Manually converting your example into BlitzMax code would be really simple: ' Define the CHAIR Type Type CHAIR Global List:TList = New TList Field X Field Y Field Height End Type ' Create 100 new chairs using FOR ... NEXT using the collection name of ROOM For tempx = 1 To 10 For tempy = 1 To 10 room:CHAIR = New CHAIR room.x = tempx room.y = tempy room.height = Rnd(0,10) ' set a random height 0 to 10 CHAIR.List.AddLast room Next Next ' Move them all over 1 (like the description example) For room:CHAIR = EachIn CHAIR.List room.x = room.x + 1 Next What your naming implies is that you're actually imagining a room full of chairs? This "List" that I added would be that room, but you are not required to put it into the Type itself. Unlike in BlitzBasic, where every Type has a single invisible list automatically attached to it, in BlitzMax you can create and use such a list anywhere, so it might make more sense to write the code like this: ' Define the CHAIR Type Type CHAIR Field X Field Y Field Height End Type ' Create a local list to put the chairs into room:TList = New TList ' Create 100 new chairs using FOR ... NEXT using the variable name "myChair", and inserting them all into the list For tempx = 1 To 10 For tempy = 1 To 10 myChair:CHAIR = New CHAIR myChair.x = tempx myChair.y = tempy myChair.height = Rnd(0,10) ' set a random height 0 to 10 room.AddLast myChair Next Next ' Move them all over 1 (like the description example) For myChair:CHAIR = EachIn room myChair.x = myChair.x + 1 Next (All of this is completely unrelated to Generics, by the way, so this is very off-topic.) |
| ||
I'm assuming that it automagically adds the new Chair to a hidden array, and "room" just references the last one in the array? Yes, looks like it is. I don't think it is more clear than what you can do in BlitzMax. Nope, in blitzmax you need to create a list first before you can iterate through your types, which is very confusing when you create a type within a type. This is why I think some hard core B3D guys cannot fully transition to Max because of some of its complexities. Why not add backwards compatibility functions for blitz3d rather than trying to copy a language like C/C++ which obviously is not Blitzmax? It seems like a trend now to upgrade a Basic languages to C/C++ way.. ;-) Sorry if I'm sort of hi-jacking this thread Brucey, but I'd just like to give some suggestions, coming from an average Max user and I have a good time using NG so far. |
| ||
Thanks FireballStarfish, that was the best and nearest blitz3d conversion that I have seen so far. Other snippets given out in the past seems to complicate things. But still that extra AddLast and Creating a New List seems a bit confusing when you got complex types. Would be nice to automatically add it like the B3D way, maybe an option if possible. |
| ||
Brucey: Okay, so something like this would be better? ... where you can just use whatever type/primitive you want? Yes, definitely, being able to use any type, and being able to know the type at runtime would be very helpful. There aren't many operations that could be done on a completely unbounded type parameter, for example calling T.ToString() wouldn't be allowed. But that's fine for writing a generic collection. If the need for a restriction to class types arises, one could express it with a constraint like <T Extends Object>, which would then make calling the basic object methods possible. (Maybe more specific constraints could be added at a later time, I'm thinking about something like "has a + operator" to allow writing generic code that can operates on an unknown number type and such...) |
| ||
But still that extra AddLast and Creating a New List seems a bit confusing when you got complex types. Not really, creating a new list and adding something to it with AddLast doesn't get any more complicated than it is in the example, no matter which Type you're doing it with. It will always be those same two commands. But the idea is that you can do it anywhere you need and with as many lists as you want. For instance you can simultaneously have a room1 and room2 that both contain different chairs for you to loop through with For-Each. Conversely, if you don't need to keep a list for one of your Types at all, you don't have to, saving on memory and speed. |
| ||
Is this like C++ templates or Java Generics? |
| ||
It is like c++ templates. But Brucey is a Java-Guy (a bit) so syntax is more Java-ish (or Monkey-ish). Bye Ron |
| ||
Azathoth: Yes, exactly. To be more precise: Java generics have a few very inconvenient limitations (which is what my earlier post #4 was about). C#-like generics, which don't have these limitations, would be really awesome to have in BlitzMax. C++ templates are even more powerful, but also more complicated. Maybe this might be a bit overkill for BlitzMax. But then again, considering BlitzMax-NG compiles to C (unlike C# which runs on a VM and can JIT-compile things), it might be a more suitable reference anyway? I suppose we wouldn't generate any code for the generic types, but each "instance" would require the "generic template" to be fully generated. So, by creating a TArrayList<int> object, a pure int-based TArrayList could be generated. bcc could keep track of what types of TArrayList had already been used, so we'd only be generating one TArrayList<int> implementation for the app/module. While I don't really have experience with C++ and its templates, some time ago, I started writing a BlitzMax precompiler. *** I also made an attempt at implementing generics there, and one of the main problems I ran into was dealing with modules and other previously compiled code. After all, generics will need to work across modules, because what use would a generic TList<T> be if it wasn't usable in "outside" code that imports the (potentially previously compiled) module? I couldn't think of an ideal solution for this, but maybe bcc and bmk will be able to deal with this more elegantly than my little precompiler. Do you already know how you want to handle this? (*** The idea was for it to accept an enhanced version of BlitzMax with a bunch of added language features that I always wished it had, and to compile it back into standard BlitzMax code. Local type inference and lambda expressions were among them, but several other things that I wanted to add, like overloading, are already supported by NG, yay!) |
| ||
Regarding modules: This is an interesting question. Doing the erasure would indeed remove the needed information during the compilation phase. Maybe the precompilates could get a sidekick-file containing needed information? But somehow I think this is a bit "dirty". @ FireballStarfish Gruesse aus Chemnitz. bye Ron |
| ||
It is like c++ templates. If the generic is just replaced with Object this sounds like Java.But Brucey is a Java-Guy (a bit) so syntax is more Java-ish (or Monkey-ish). C++ templates are more powerful in that they allow for specialization. http://en.cppreference.com/w/cpp/language/template_specialization |
| ||
I've been lurking for a while... but here's something I've been working on:SuperStrict Framework brl.standardio Local intStack:TStack<Int> = New TStack<Int>() For Local i:Int = 0 Until 10 intStack.Push(i) Next For Local i:Int = 0 Until 10 Print intStack.Pop() Next Type TStack<T> Field items:T[] Field stackPointer:Int Method New(initialCapacity:Int = 16) items = New T[initialCapacity] End Method Method Push(item:T) If stackPointer >= items.Length items = items[..items.Length + items.Length * 0.5] End If items[stackPointer] = item stackPointer :+ 1 End Method Method Pop:T() If stackPointer > 0 Then stackPointer :- 1 Local item:T = items[stackPointer] items[stackPointer] = Null Return item End If Return Null End Method End Type which outputs 9 8 7 6 5 4 3 2 1 0 Kinda cool, I think. And you can also do this: Local stringStack:TStack<String> = New TStack<String>() stringStack.Push("Hello") stringStack.Push("World") Print stringStack.Pop() Print stringStack.Pop() which outputs World Hello You'll note that in theory you can use *any* type (not just Objects!), which opens up things like Lists of Ints, or Maps of pointers. etc. .. but it's early days, with lots of stuff to fix before it starts to work properly - especially cross-import support, which I as yet don't have a plan for. I have pondered some ideas for this, most of which involves the .i file containing some compressed version of the generic type that can be re-parsed where required - either the source or a serialised ast... (ideas on a postcard ... ) But nice to have the possibility that we may have generics in BlitzMax at some point... |
| ||
Cool! |
| ||
A small example with a generic interface :SuperStrict Framework brl.standardio Local p1:IPair<String, Int> = New TOrderedPair<String, Int>("Even", 8) Print p1.getKey() + " : " + p1.getValue() Interface IPair<K, V> Method getKey:K() Method getValue:V() End Interface Type TOrderedPair<K, V> Implements IPair<K, V> Field key:K Field value:V Method New(key:K, value:V) Self.key = key Self.value = value End Method Method getKey:K() Return key End Method Method getValue:V() Return value End Method End Type Outputs Even : 8 |
| ||
Hey Brucey, Sounds like an excellent feature. I've not been using NG for a little while but I can see me using this feature when I do. Thankyou for the addition! |