Generics

BlitzMax Forums/BlitzMax NG/Generics

Brucey(Posted 2017) [#1]
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 :-)


grable(Posted 2017) [#2]
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?


GW(Posted 2017) [#3]
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)



FireballStarfish(Posted 2017) [#4]
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.


RustyKristi(Posted 2017) [#5]
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.


col(Posted 2017) [#6]
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>



RustyKristi(Posted 2017) [#7]
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.


Brucey(Posted 2017) [#8]
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.


FireballStarfish(Posted 2017) [#9]
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.)


RustyKristi(Posted 2017) [#10]
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.


RustyKristi(Posted 2017) [#11]
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.


FireballStarfish(Posted 2017) [#12]
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...)


FireballStarfish(Posted 2017) [#13]
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.


Azathoth(Posted 2017) [#14]
Is this like C++ templates or Java Generics?


Derron(Posted 2017) [#15]
It is like c++ templates.
But Brucey is a Java-Guy (a bit) so syntax is more Java-ish (or Monkey-ish).


Bye
Ron


FireballStarfish(Posted 2017) [#16]
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!)


Derron(Posted 2017) [#17]
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


Azathoth(Posted 2017) [#18]
It is like c++ templates.
But Brucey is a Java-Guy (a bit) so syntax is more Java-ish (or Monkey-ish).
If the generic is just replaced with Object this sounds like Java.

C++ templates are more powerful in that they allow for specialization.
http://en.cppreference.com/w/cpp/language/template_specialization


Brucey(Posted May) [#19]
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...


Sub_Zero(Posted May) [#20]
Cool!


Brucey(Posted May) [#21]
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



col(Posted May) [#22]
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!