Generic Objects in twinBASIC

To learn more about twinBASIC, join me at this year's virtual Access DevCon where I will be presenting this exciting new project from vbWatchdog creator, Wayne Phillips.

Generic objects in twinBASIC

One of the exciting new features in the twinBASIC language is its support for generic objects.  In a nutshell, generics allow you to pass types as parameters in addition to simply passing values.

This is particularly useful if you want to define a common data structure in the form of a class.  The class won't care what type(s) you pass to it, so long as you are consistent with the types for a given instance of the class.

Generic programming can be a bit difficult to wrap your head around.  Rather than spend this article discussing it, I'll let you follow that link and go down the Wikipedia rabbit hole on your own.

What I'm interested in here is showing you a quick practical example of a generic class in twinBASIC.  In doing so, I hope you'll see how powerful this feature can be given the right circumstances.

The Right Circumstances

The Dictionary object in the Microsoft Scripting Runtime is a hash table structure that lets you store data as key-value pairs.  The object is a bit unsafe, though, as the key and value are both Variant types.  Because of this, you can use absolutely anything as both key and value.  You can even mix types within the same instance of the class.  

This is madness.  Let's put a stop to it.

The Code

The sample below is not a full implementation of a generic dictionary class; I'll leave that as an exercise for the reader (or maybe the subject of a future article).  Rather, it's just enough to demonstrate the syntax of generic objects in twinBASIC so that you can do your own experimenting.

Class Dict (Of TKey, TValue)
    Dim mDict As Scripting.Dictionary
    
    Public Sub New()
        Set mDict = New Scripting.Dictionary
    End Sub
    
    Public Sub Add(Key As TKey, Value As TValue)
        mDict.Add(Key, Value)        
    End Sub
    
    Public Property Get Item(Key As TKey) As TValue
        Return mDict.Item(Key)
    End Property
End Class

The new syntax that makes this whole thing work when defining a generic class is the Class <ClassName> (Of <TypeA>, <TypeB>, <TypeC>) declaration line.  Notice that the types that we define by name in that line can then be referenced elsewhere within the class when defining properties and methods:

Naming conventions

A common naming convention is to use the single capital letter 'T' to represent the type if there is only a single type accepted by the generic class.  If there are multiple types (as in our example above), the convention is to use a leading capital 'T' followed by a descriptive name for the type.  

In this case, the first type variable, TKey, refers to the "type of the key" our dictionary will accept.  The second variable, TValue, refers to the "type of the value" our dictionary will accept.  

Using the class

Let's start with a simple example.  We will create a dictionary that uses a long integer for a key and a string as the value.

Sub TestDict()
    Dim Lookup As Dict(Of Long, String)
    Set Lookup = New Dict(Of Long, String)
    
    Lookup.Add 1, "A"
    Lookup.Add 5, "E"
    Lookup.Add 9, "I"
    
    Debug.Print Lookup.Item(5)
End Sub

Notice that we need to provide the types both when declaring the object (the Dim Lookup line) and when creating an instance of the object (the Set Lookup line).

Reusing the class

This is where we get to see why we would write generics in the first place.  Let's say we want to create a different kind of lookup table.  This one will be keyed using a string.  The string key will be used to retrieve an oVehicle object.

Sub TestVehicleDict()
    Dim CarLookup As New Dict(Of String, oVehicle)
    
    CarLookup.Add "Bullitt", NewVehicleObject("Ford", "Mustang", 1968)
    CarLookup.Add "ECTO-1", NewVehicleObject("Cadillac", "Miller-Meteor", 1950)
    
    Debug.Print CarLookup.Item("Bullitt").Year
    Debug.Print CarLookup.Item("ECTO-1").Model
End Sub

In this example, we used the As New syntax to automatically generate an instance of the variable the first time it is referenced.  This works just as well with our generic object as the Dim/Set approach we took in the previous example.

And just to prove that the compiler is actually enforcing the type safety, let's try calling CarLookup.Add() with the same arguments we used to call Lookup.Add():

Nice try, sucka!

The compiler doesn't complain about using 1 as the key, since twinBASIC can implicitly coerce that into the String the CarLookup object expects.  However, it is not able to coerce "A" into an oVehicle object, and it makes sure we know it.

Avoiding subtle bugs with the added type safety

Using a fully-implemented version of this generic dictionary class in place of the anything-goes default version found in the scripting runtime can help us avoid subtle bugs like this one:

The Subtle Dictionary Key Bug
Always explicitly call the .Value property when using fields or controls as Dictionary keys. Else the bugs come crawling!

In the above article, I detailed a bug that can appear if you use a textbox control as the key to a standard dictionary.  As a developer, you may assume that the default .Value property of the control is being passed to the dictionary.  But in fact, it's passing a reference to the textbox object itself and not the textbox's value.  This goes back to the fact that the Key argument is a Variant, so literally anything you pass will be used as the key.

Here's the problematic code from that article:

Set EmployeePhoneNums = CreateObject("Scripting.Dictionary")
Me.tbLastName.Value = "Jones"
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-1234"
Me.tbLastName.Value = "Smith"
EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-6789"

To avoid the bug completely, we could simply change the first line from...

Set EmployeePhoneNums = CreateObject("Scripting.Dictionary")

...to...

Set EmployeePhoneNums = New Dict(Of String, String)

Now, when the following line gets called...

EmployeePhoneNums.Add Key:=Me.tbLastName, Item:="555-1234"

...twinBASIC will see that the key must be a String. It will then coerce the textbox object (Me.tbLastName) into a String by fetching its default property, .Value, and using that as the key.  Presto change-o, the bug is gone!

Full example with semantic highlighting

Here's a screenshot of the full twinBASIC code with all that juicy semantic highlighting goodness:

Image by Francesco Romeo from Pixabay