Bending Vb6 in the functional direction

Update:Code available and developed further on GitHub

A substantial amount of my day to day work is spent maintaining a legacy VB6 codebase.
Unfortunately I also create new features for our users on slightly less ancient platforms.

This has the unwanted side effect that back in the land of VB6 i miss some of the language features from the newer platforms.

The feature I miss the most is probably an efficient way of working with collections.
In C# there's linq, in most functional languages there are som variant of map/reduce functionality.

In VB6 you have loops.

For Each and Every One

If you want to do anything with a collection of something in VB6 you better be prepared to do some serious looping.

'Creating a new collection with only one property from the original object
Dim item as Variant
Dim newCollection as Collections
Set newCollection = new Collection
for each item in originalCollection
        newCollection.Add item.Property
next item

In C# the equivalent would be

var newCollection = originalCollection.Select(item => item.Property);

And to throw in some functional

let newCollection = originalCollection |> List.map (fun x -> x.Property)

Or another example, to concatenate the items in a collection

VB6:

Dim item as Variant
Dim concatenated as string : concatenated = ""
Dim separator as string : separator = ""

for each item in newCollection
        concatenated = concatenated + separator + Cstr(item)
        separator = ","
next item

C#

var concatenated = newCollection.Aggregate((accumulated,listElement) => string.format("{0},{1}",accu,ulated,listElement));

And F#

let concatenated = newCollection |> List.reduce (fun accumulated listElement -> sprintf "%s,%s" accumulated listElement)

All the cool kids are doing it

I find the functional approach much easier on the eyes, and very much more satisfying to write. (A point I don't think should be underestimated).
And to quote Scott Hanselman (on a completely unrelated subject) "There are a finite number of keystrokes left in your hands before you die" (http://www.hanselman.com/blog/DoTheyDeserveTheGiftOfYourKeystrokes.aspx). I don't want to spend them writing loops.

So lets try

In a severe case of yak shaving I set out to see if it was at all possible to get close to the functional map/reduce pattern with VB6.

The answer is: kind of, but not quite

For the map/reduce pattern to be anything near elegant, it needs to be able to reference a function to be used for the mapping or reducing (In my examples above I've used inline lambda expressions, but they could just as easily have been proper methods (or functions) referred by name). Vb6 of course lacks most of what is needed to achieve this. (I do really want to be corrected on this in the comments, pretty please?) There are no inline lambdas, there are no function delegates and there are no proper reflection to fake functional delegates with string names.

I can haz delegates?

I did however find a reference to something called function pointers, described in the book "Advanced Visual Basic 6: power techniqued for everyday programs by Matthew Curland (example impementation here: http://www.bvbcode.com/code/70ibveu9-276757)
But this solution used scary stuff like pointers and assembly language that I'm too young and uneducated to determine whether it's safe or not to use in our environment. If anyone want to educate me on this or similiar subjects I'd listen, cause having proper delegates in Vb6 would be great.

No you can't

I ended up with a solution based on the trusty old CallByName (that I've mentioned in an earlier post.).
This of course has it's limitations:

  • Functions must be methods on an object. Meaning procedures on a form, or class object. Implicitly, functions in a module (.bas) file can not be used.
  • Functions must be public methods on said object. Private methods will not be available

But this isn't too bad, is it?

The result, above example using Map/reduce a'la VB6:

Dim concatenated as string
concatenated = List.From(originalCollection).SelectProperty("Property").Concat(",")

I've cheated a bit, as "SelectProperty" and "Concat" are really helper wrappers around a more generic Map/Reduce set of functions.
The fully verbose version would be like this:

'This is a form, just to be an object
option explicit

private sub Form_load()
        dim concatenated as string     

        set originalCollection = ()
        concatenated = Cstr(List.From(InitializeCollection).Map("MapProperty").Fold(Me,"Concatenate",""))
        msgbox concatenated
end sub

public function MapProperty(item) as variant
        MapProperty = item.Property
end function

public function Concatenate(concatenated, item) as variant
        if lenb(concatenated) = 0 then
                concatenated = Cstr(item)
    else
                concatenated = concatenated + "," + Cstr(item)
        end if
        Concatenate = concatenated
end function

private function InitializeCollection() as Collection
        dim coll as collection
        set coll = new collection
        'fill collection with some data
       set InitializeColleciton = coll
end function

I do see that this is very verbose, and at first more typing than the simple for each loop that would have done the trick. But to my defense: In a larger solution there are repeating patterns, and with a couple of helper methods many repeating tasks can be solved in one line.
And did I mention: It's more fun!
A last caveat before the code: my implementation is not tuned for performance (an not tested on larger collections).

Implementation

In the end: here is my implementation for (a very naive) Map/reduce in Vb6:

1 The meat, a lst class with methods to create a somewhat fluent interface:

Option Explicit

Private internalCollection As Collection
Private internalConcatenationSeparator As String
Private internalPropertyName As String

Private Function item(ByVal Index As Variant) As Variant
  If IsObject(internalCollection(Index)) Then
    Set item = internalCollection(Index)
  Else
    item = internalCollection(Index)
  End If
End Function

Public Property Get NewEnum() As IUnknown
  Set NewEnum = internalCollection.[_NewEnum]
End Property

Public Property Get Count() As Long
  Count = internalCollection.Count
End Property

Private Sub Class_initialize()
  Set internalCollection = New Collection
  internalConcatenationSeparator = ""
End Sub

Public Sub Add(item As Variant)
  internalCollection.Add item
End Sub

Private Sub Remove(Index As Variant)
  internalCollection.Remove Index
End Sub

Private Sub Class_terminate()
  Set internalCollection = Nothing
End Sub

Public Function Map(objectContainingMappingFunction, nameOfMappingFunction) As Lst
  Dim newList As Lst
  Dim entry As Variant
  Set newList = New Lst
  For Each entry In internalCollection
    newList.Add CallByName(objectContainingMappingFunction, nameOfMappingFunction, VbMethod, entry)
  Next entry
  Set Map = newList
End Function

Public Function Fold(objectContainingFoldingFunction, nameOfFoldingFunction, ByVal initialValue) As Variant
  Dim foldingResult
  Dim entry As Variant
 
  foldingResult = initialValue
  For Each entry In internalCollection
    If IsObject(foldingResult) Then
      Set foldingResult = CallByName(objectContainingFoldingFunction, nameOfFoldingFunction, VbMethod, foldingResult, entry)
    Else
      foldingResult = CallByName(objectContainingFoldingFunction, nameOfFoldingFunction, VbMethod, foldingResult, entry)
    End If
  Next entry
 
  If IsObject(foldingResult) Then
    Set Fold = foldingResult
  Else
    Fold = foldingResult
  End If
End Function

Public Function Concat(Optional separator As String = "") As String
  internalConcatenationSeparator = separator
  Concat = CStr(Me.Fold(Me, "internalConcatenator", ""))
End Function

Public Function internalConcatenator(concatenated, entry) As Variant
  internalConcatenator = concatenated
  If IsObject(entry) Then
    If entry Is Nothing Then Exit Function
  End If
  If LenB(concatenated) = 0 Then
    concatenated = CStr(entry)
  Else
    concatenated = concatenated + internalConcatenationSeparator + CStr(entry)
  End If
  internalConcatenator = concatenated
End Function

Public Function SelectProperty(nameOfProperty As String) As Lst
  internalPropertyName = nameOfProperty
  Set SelectProperty = Me.Map(Me, "internalPropertySelector")
End Function

Public Function internalPropertySelector(entry) As Variant
  If IsObject(CallByName(entry, internalPropertyName, VbGet)) Then
    Set internalPropertySelector = CallByName(entry, internalPropertyName, VbGet)
  Else
    internalPropertySelector = CallByName(entry, internalPropertyName, VbGet)
  End If
End Function

And a static entry module to loose boring class initialization:

Option Explicit

Public Function From(enumerable) As Lst
  Dim item As Variant
  Dim newList As Lst
  Set newList = New Lst
  For Each item In enumerable
    newList.Add item
  Next item
  Set From = newList
End Function

The End.

This is a naive start, It may or may not will probably be continually developed on github: https://github.com/Vidarls/vb6utils Do check it out, and maybe even help improve it?

Categories: