Bending Vb6 in the functional direction
Submitted by Vidar on

Photo by Dennis Jarvis (CC license)
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.
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
And to throw in some functional
Or another example, to concatenate the items in a collection
VB6:
Dim concatenated as string : concatenated = ""
Dim separator as string : separator = ""
for each item in newCollection
concatenated = concatenated + separator + Cstr(item)
separator = ","
next item
C#
And F#
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:
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:
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:
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:
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?