Lambda Expressions
I've already posted this library elsewhere but figured that people on VBForums would find it useful too! This is currently only written to work in VBA but I believe a port to VB6 would only require the alteration of a few declarations. Or perhaps in quite a few within evaluateFunc... Happy to have pull requests if anyone wants to make it usable in VB6!
What is a lambda expression?
A lambda expression/anonymous function is a function definition that is not bound to a name. Lambda expressions are usually "1st class citezens" which means they can be passed to other functions for evaluation.
I personally believe this is best described with an example. Imagine we wanted to sort an array of sheets by their name. In VBA this would be relatively complex and require an understanding of how to sort data in the first place, as well as which algorithms to use. Lambda allows us to define 1 sorting function and then provide our lambda function to provide the ID to sort on:
Download
The file can be found on github here:
stdLambda.cls.
stdICallable will also be required: stdICallable.cls
How to use stdLambda
The Create() constructor is the main way to create an instance of the stdLambda object.
To define a function which takes multiple arguments $# should be used where # is the index of the argument. E.G. $1 is the first argument, $2 is the 2nd argument and $n is the nth argument.
You can also define functions which call members of objects. Use xxx#xxx() to call functions and xxx.xxx() to call properties.
The lambda syntax comes with many VBA functions which you are already used to...
As well as an inline if statement:
With stdLambda you are not limited to a single lines, you can also use multiple lines. Note the result of the last line in the lambda is returned:
You can also use variables, much like in VB6:
Finally you can use Function definitions if you want to use recursion:
Evaluating lambdas
Use default member execution:
Use Run method:
Use RunEx method, supplying an array of arguments:
Sometimes it's useful to use an interface. In this case use stdICallable interface:
An update as of 16/09/2020 added the Bind() method to stdLambda as well. The Bind() method creates a new ICallable that, when called, supplies the given sequence of arguments preceding any provided when the new function is called. This ultimately saves on expression compilation time.
How it works
Finally, how does the class work internally?
Create first looks to see if a lambda already exists, if it does it is returned, else it calls Init which:
Then when an expression is executed, Run calls evaluate which:
Integration with the STD-VBA Library
Thought i'd give a taste of one of the core reasons I built this library!
Long term goals
The intermediate representation is good, but it would be even better if we could compile to machine code... I'm pretty sure this is even more difficult, but in the pursuit of speed that's maybe where we'll have to go!
Happy Coding!
~Sancarn
I've already posted this library elsewhere but figured that people on VBForums would find it useful too! This is currently only written to work in VBA but I believe a port to VB6 would only require the alteration of a few declarations. Or perhaps in quite a few within evaluateFunc... Happy to have pull requests if anyone wants to make it usable in VB6!
What is a lambda expression?
A lambda expression/anonymous function is a function definition that is not bound to a name. Lambda expressions are usually "1st class citezens" which means they can be passed to other functions for evaluation.
I personally believe this is best described with an example. Imagine we wanted to sort an array of sheets by their name. In VBA this would be relatively complex and require an understanding of how to sort data in the first place, as well as which algorithms to use. Lambda allows us to define 1 sorting function and then provide our lambda function to provide the ID to sort on:
Example.bas Code:
Sub Main myArray = Array(Sheets(1),Sheets(2)) newArray = sort(myArray, stdLambda.Create("$1.name")) End Sub Function sort(array as variant, accessor as stdICallable) '... sorting code ... elementID = accessor(element) '... sorting code ... End Function
Download
The file can be found on github here:
stdLambda.cls.
stdICallable will also be required: stdICallable.cls
How to use stdLambda
The Create() constructor is the main way to create an instance of the stdLambda object.
Example.bas Code:
Sub test() Dim cb as stdLambda set cb = stdLambda.Create("1+1") End Sub
To define a function which takes multiple arguments $# should be used where # is the index of the argument. E.G. $1 is the first argument, $2 is the 2nd argument and $n is the nth argument.
Example.bas Code:
Sub test() Dim average as stdLambda set average = stdLambda.Create("($1+$2)/2") End Sub
You can also define functions which call members of objects. Use xxx#xxx() to call functions and xxx.xxx() to call properties.
Example.bas Code:
Sub test() Debug.Print stdLambda.Create("$1.Name")(someObject) 'returns ThisWorkbook.Name Call stdLambda.Create("$1#Save")(someObject) 'calls ThisWorkbook.Save End Sub
The lambda syntax comes with many VBA functions which you are already used to...
Example.bas Code:
Sub test() Debug.Print stdLambda.Create("Mid($1,1,5)")("hello world") 'returns "hello" Debug.Print stdLambda.Create("$1 like ""hello*""")("hello world") 'returns true End Sub
As well as an inline if statement:
Example.bas Code:
Sub test() Debug.Print stdLambda.Create("if $1 then 1 else 2")(true) 'returns 1 Debug.Print stdLambda.Create("if $1 then 1 else 2")(false) 'returns 2 'Note: this will only call someObj.CallMethod() and will not call someObj.CallMethod2() (unless 1st arg is supplied as false of course) Debug.Print stdLambda.Create("if $1 then $2#CallMethod() else $2#CallMethod2()")(true,someObj) End Sub
With stdLambda you are not limited to a single lines, you can also use multiple lines. Note the result of the last line in the lambda is returned:
Example.bas Code:
Call stdLambda.Create("2+2: 5*2").Run() '... or ... Call stdLambda.CreateMultiline(array( _ "2+2", _ "5*2", _ )).Run()
You can also use variables, much like in VB6:
Example.bas Code:
'the last assignment is redundant, just used to show that assignments result in their value Debug.Print stdLambda.CreateMultiline(array( _ "count = $1", _ "footPrint = count * 2 ^ count" _ )).Run(2) ' -> 8
Finally you can use Function definitions if you want to use recursion:
Example.bas Code:
stdLambda.CreateMultiline(Array( _ "fun fib(v)", _ " if v<=1 then", _ " v", _ " else ", _ " fib(v-2) + fib(v-1)", _ " end", _ "end", _ "fib($1)" _ )).Run(20) '->6765
Evaluating lambdas
Use default member execution:
Example.bas Code:
Sub test() Dim average as stdLambda set average = stdLambda.Create("($1+$2)/2") Debug.Print average(1,2) '1.5 End Sub
Use Run method:
Example.bas Code:
Sub test() Dim average as stdLambda set average = stdLambda.Create("($1+$2)/2") Debug.Print average.Run(1,2) '1.5 End Sub
Use RunEx method, supplying an array of arguments:
Example.bas Code:
Sub test() Dim average as stdLambda set average = stdLambda.Create("($1+$2)/2") Debug.Print average.RunEx(Array(1,2)) '1.5 End Sub
Sometimes it's useful to use an interface. In this case use stdICallable interface:
Example.bas Code:
Sub test(ByVal func as stdICallable) func.Run(ThisWorkbook, 1, "hello world") End Sub
An update as of 16/09/2020 added the Bind() method to stdLambda as well. The Bind() method creates a new ICallable that, when called, supplies the given sequence of arguments preceding any provided when the new function is called. This ultimately saves on expression compilation time.
Example.bas Code:
'Expression created, argument bound. Dim cb as stdLambda: set cb = stdLambda.Create("$1 + $2").Bind(5) Debug.Print cb(1) '6 Debug.Print cb(2) '7 Debug.Print cb(3) '8 'No compilation required, cached lambda is used with new bound argument set cb = stdLambda.Create("$1 + $2").Bind(6) Debug.Print cb(1) '7 Debug.Print cb(2) '8 Debug.Print cb(3) '9
How it works
Finally, how does the class work internally?
Create first looks to see if a lambda already exists, if it does it is returned, else it calls Init which:
- Tokenises the string using Regex
- Calls parseBlock() which uses a top-down parsing algorithm to parse the entire block to an array/stack containing operations (i.e. compiles to byte code)
Then when an expression is executed, Run calls evaluate which:
- Loops over all operations, detects the type and subtype of the operation
- Performs the operations function
- After all operations have executed the 1st item in the stack is returned.
Integration with the STD-VBA Library
Thought i'd give a taste of one of the core reasons I built this library!
Example.bas Code:
'Create an array Dim arr as stdArray set arr = stdArray.Create(1,2,3,4,5,6,7,8,9,10) 'Can also call CreateFromArray 'More advanced behaviour when including callbacks! And VBA Lamdas!! Debug.Print arr.Map(stdLambda.Create("$1+1")).join '2,3,4,5,6,7,8,9,10,11 Debug.Print arr.Reduce(stdLambda.Create("$1+$2")) '55 ' I.E. Calculate the sum Debug.Print arr.Reduce(stdLambda.Create("Max($1,$2)")) '10 ' I.E. Calculate the maximum Debug.Print arr.Filter(stdLambda.Create("$1>=5")).join '5,6,7,8,9,10 'Execute property accessors with Lambda syntax Debug.Print arr.Map(stdLambda.Create("ThisWorkbook.Sheets($1)")) _ .Map(stdLambda.Create("$1.Name")).join(",") 'Sheet1,Sheet2,Sheet3,...,Sheet10 'Execute methods with lambda: Call stdArray.Create(Workbooks(1),Workbooks(2)).forEach(stdLambda.Create("$1#Save") 'Sort objects by date, and then print names concatenated with comma Debug.Print stdArray.Create(ObjA,ObjB,ObjC,ObjD,ObjE).sort(stdLambda.Create("$1.Date")).map(stdLambda.Create("$1.Name")).join(",") 'We even have if statement! With stdLambda.Create("if $1 then ""lisa"" else ""bart""") Debug.Print .Run(true) 'lisa Debug.Print .Run(false) 'bart End With
Long term goals
The intermediate representation is good, but it would be even better if we could compile to machine code... I'm pretty sure this is even more difficult, but in the pursuit of speed that's maybe where we'll have to go!
Happy Coding!
~Sancarn