- Introduction
- What is a Lambda expression
- Spec
- Syntax
- Expression evaluation
- Parameter Access
- Dictionary member access
- Usage of in-built functions
- Multi-line lambda expressions.
- If statements and Inline-if statements
- Variable definition
- Function definition
- Internal Methods and Variables
- ...
- Constructors
- Create()
- CreateMultiline()
- stdICallable
- Run()
- RunEx()
- Bind()
- SendMessage()
- Instance Methods
- Bind()
- BindEx()
- BindGlobal()
- Run()
- RunEx()
- Syntax
A lambda expression/anonymous function is a function definition that is not bound to a name. Lambda expressions are usually "1st class citizens" which means they can be passed to other functions for evaluation.
Lambda expressions are often best described with an example. Imagine we wanted to filter the Worksheets object for a Worksheet by Name, Visibility and the Data in Range("A1"). Typically in VBA we'd do this as follows:
Function FilterEnumByName(ByRef col as collection, ByVal sNameLike as string) as collection
Dim ret as Collection: set ret = new Collection
Dim item as object
For each item in col
if item.name like sNameLike then
ret.add item
end if
next
set FilterEnumByName = ret
End Function
Function FilterEnumByVisible(ByRef col as collection, ByVal bVisible as Boolean) as collection
Dim ret as Collection: set ret = new Collection
Dim item as object
For each item in col
if item.name = bVisible then
ret.add item
end if
next
set FilterEnumByVisible = ret
End Function
Function FilterEnumByRange(ByRef col as collection, ByVal v as Variant) as collection
Dim ret as Collection: set ret = new Collection
Dim item as object
For each item in col
if item.range("A1").value = v then
ret.add item
end if
next
set FilterEnumByRange = ret
End FunctionIn this scenario we have made 3 seperate functions, and each function is operationally, virtually the same but performing slightly different operations. Furthermore the operation of each function is extremely limited. This also breaks the DRY principle in programming.
Wouldn't it be better if we could get all that power while only writing a single function? Well, we can, with lambda expressions:
Sub Test
'Filter by name
set col = FilterEnum(col, stdLambda.Create("$1.name like ""Hello*"""))
'Filter by Visible
set col = FilterEnum(col, stdLambda.Create("not $1.Visible"))
'Filter by Range
set col = FilterEnum(col, stdLambda.Create("$1.Range(""A1"").value = ""Test"""))
End Sub
Function FilterEnum(ByRef col as Collection, ByVal lambda as stdICallable) as Collection
Dim ret as Collection: set ret = new Collection
Dim item as object
For each item in col
if lambda.run(item) then
ret.add item
end if
next
set FilterEnumByRange = ret
End FunctionBare in mind, however, that this new function is significantly more powerful! If you suddenly also need to filter where range("A3").value < 10 this is something that can be easily applied:
'...
set col = FilterEnum(col, stdLambda.Create("$1.Range(""A3"").Value < 10"))
'...This is the role that Lambda expressions provide, and is what stdLambda provides within VBA.
One of the core use cases of stdLambda in stdVBA can be found in stdArray, an array class declared in src/stdArray.cls. The following examples can be used to see the use of the stdLambda class with stdArray:
'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")
'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 WithOne of the core additions in stdLambda is the custom syntax/language embedded in VBA strings.
stdLambda's primary focus is expression evaluation.
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.
Sub test()
Dim average as stdLambda
set average = stdLambda.Create("($1+$2)/2")
End SubYou can also define functions which call members of objects. E.G.
Sub test()
'Call properties or methods with `.`
Debug.Print stdLambda.Create("$1.Name")(ThisWorkbook) 'returns ThisWorkbook.Name
Call stdLambda.Create("$1.Save")(ThisWorkbook) 'calls ThisWorkbook.Save
'If you absolutely need to use `VbGet` use `.$` or if you need to use `VbMethod` use `.#` instead.
Debug.Print stdLambda.Create("$1.$Name")(ThisWorkbook) 'returns ThisWorkbook.Name
Call stdLambda.Create("$1.#Save")(ThisWorkbook) 'calls ThisWorkbook.Save
End Sub
If a dictionary is passed into a stdLambda then you can use Dictionary.Key syntax to access members:
'Both:
stdLambda.create("$1.item(""someVar"")").run(myDict)
'and
stdLambda.create("$1.someVar").run(myDict)
'return the same resultsThe lambda syntax comes with many in-built functions many of which call directly to native VBA functions. These can greatly help productivity.
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 SubLambda expressions may be defined as multi-line expressions using either the : end-of-line character or using CreateMultiLine constructor. In the cases where a mutli-line function is used, the last expression evaluated is returned from the lambda. I.E. the following expressions will return 10:
Call stdLambda.Create("2+2: 5*2").Run()
'... or ...
Call stdLambda.CreateMultiline(array( _
"2+2", _
"5*2"
)).Run()stdLambda has an if statement as well as an inline-if statement. The if statement looks as follows:
With stdLambda.CreateMultiline(array( _
"if $1 = 1 then", _
" 5*1", _
"else if $1 = 2 then", _
" 5*2", _
"else", _
" 5*3", _
"end"
))
Debug.Print .Run(0) '15
Debug.Print .Run(1) '5
Debug.Print .Run(2) '10
Debug.Print .Run(3) '15
End WithThe inline-if acts as a regular if statement, It will only execute the then ... part or else ... part, but it will 'return' the result of the executed block. This means it can be used inline like vba's iif can be used, but it doesn't have to compute both the then and else part like iif does.
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
End Sub
' only evaluates doSmth(), does not evaluate doAnother() when $1 is true, and visa versa
stdLambda.Create("(if $1 then doSmth() else doAnother() end)").Run(True)note: if statements will only evaluate the part which is required. This is extremely beneficial and far superior in comparrison to
iif()
Variables can be defined and assigned, e.g. oranges = 2. This can make definition of extensive formula easier to read. Assignment results in their value.
'the last assignment is redundant, just used to show that assignments result in their value
Debug.Print stdLambda.CreateMultiline(array( _
"let count = $1", _
"let footPrint = count * 2 ^ count" _
)).Run(2) ' -> 8You can also define functions:
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- Currently the main "caveat" to using this library is performance. This will absolutely not be as fast as pure VBA code and doesn't intend to be.
- Currently there is a lack of error handling.
- Lack of intellisense or syntax highlighting. This can be fixed in IDEs like VSCode.
In stdLambda functions and variables are treated as the same thing. Variable names which don't exist in an assignment, won't make it onto the variable table, and thus won't make it onto the stack, and are interpreted as iType.oFunc types instead of iType.oAccess. At runtime iType.oFunc code is passed to the evaluateFunc function with arguments also received from the stack, if a variable is evaluated with evaluateFunc, typically no arguments are passed. Within evaluateFunc, first the function extension table is checked to see if the variable name exists in there, otherwise the variable name is looked up in a large select-case tree.
Co-incidentally BindGlobal just adds to the function extension table in evaluateFunc. Also this means that bindGlobal can be used to bind a function to a lambda expression, and also can be used to override built-in functions and variables.
All function names in stdLambda are case insensitive, a design decision to make the language more like VBA.
Evaluates to the workbook which instantiated the stdLambda class.
ThisWorkbook.nameEvaluates to the Excel/Word/Powerpoint/App's Application object.
if Application.version < 15 then 1 else 2Evaluates an expression passed as a string. Note: Internally eval uses stdLambda. The definition in source is stdLambda.Create(firstArg).Run()
eval("1+1") '==> 2A call to VBA's Abs() function.
A call to VBA's Int() function.
A call to VBA's Fix() function.
A call to VBA's Exp() function.
A call to VBA's Log() function.
A call to VBA's Sqr() function.
A call to VBA's Sgn() function.
A call to VBA's Rnd() function.
A call to VBA's Cos() function. angle should be defined in radians.
A call to VBA's Sin() function. angle should be defined in radians.
A call to VBA's Tab() function. angle should be defined in radians.
A call to VBA's Atn() function. Calculates the Inverse tangent. angle should be defined in radians.
Calculates the Inverse sin. angle should be defined in radians.
Calculates the Inverse cosine. angle should be defined in radians.
Creates an array from the supplied parameters:
Array(1,2,3) '--> Array(1,2,3)A call to VBA's CreateObject() function.
CreateObject("Scripting.Dictionary")A call to VBA's GetObject() function.
GetObject("InternetExplorer.Application")A call to VBA's Iff() function. It is suggested that you avoid calls to this function and instead use the in-line if of stdLambda.
A call to VBA's TypeName(v as variant) as string function.
A call to VBA's cbool function.
A call to VBA's cbyte function.
A call to VBA's ccur function.
A call to VBA's cdate function.
definitionA call to VBA's csng function.
A call to VBA's cdbl function.
A call to VBA's cint function.
A call to VBA's clng function.
A call to VBA's cstr function.
A call to VBA's cvar function.
A call to VBA's cverr function.
A call to VBA's Asc function.
A call to VBA's Chr function.
A call to VBA's Format function.
A call to VBA's Hex function.
A call to VBA's Oct function.
A call to VBA's Str function.
A call to VBA's Val function.
A call to VBA's Trim function.
A call to VBA's LCase function.
A call to VBA's UCase function.
A call to VBA's Right function.
A call to VBA's Left function.
A call to VBA's Mid function.
A call to VBA's Len function.
A call to VBA's Now function.
A call to VBA's Switch function.
A call to VBA's Any function.
Returns a carriage-return line-feed.
Returns a carriage-return.
Returns a line-feed.
Returns a carriage-return line-feed.
Returns a null character.
Returns a null string.
Equivalent of vbObjectError in VBA
Returns a tab character.
Returns a backspace character.
Returns a form feed character
Returns a vertical tab character
Create(ByVal sEquation As String, Optional ByVal bUsePerformanceCache As Boolean = False, Optional ByVal bSandboxExtras As Boolean = False) As stdLambda
Creates and returns a stdLambda object which will execute the supplied equation body, when run.
Debug.Print stdLambda.Create("1+3*8/2*(2+2+3)").Execute()
With stdLambda.Create("$1+1+3*8/2*(2+2+3)")
Debug.Print .Execute(10)
Debug.Print .Execute(15)
Debug.Print .Execute(20)
End With
Debug.Print stdLambda.Create("$1.Range(""A1"")").Execute(Sheets(1)).Address(True, True, xlA1, True)
Debug.Print stdLambda.Create("$1.join("","")").Execute(stdArray.Create(1,2))Use bUsePerformanceCache when looping over a large dataset (e.g. many rows of a table) and filtering on something very specific with little variance. Example:
'Data like
'| ID | Type | Status | ...
'|----|---------|----------| ...
'| 1 | Message | Archived | ...
'| 2 | Note | Active | ...
'| 3 | Message | Active | ...
'| 4 | Message | Active | ...
'| 5 | Note | Active | ...
'...
'Assuming our filterByColumns function passes only the columns values into the lambda as parameters, the following will be much faster:
myTable.filterByColumns(Array("Type", "Status"), stdLambda.Create("$1=""Message"" and $2=""Active""", true))
'than
myTable.filterByColumns(Array("Type", "Status"), stdLambda.Create("$1=""Message"" and $2=""Active"""))Note:
Avoid using Performance cache when lambda is called with very dynamic arguments and/or on small sets of data. E.G.
'Data like
'| Freq | Risk | ...
'|-------|---------| ...
'| 1.2 | 5.0 | ...
'| 2.1 | 5.0 | ...
'| 34.2 | 2.4 | ...
'| 43.2 | 2.7 | ...
'| 2.0 | 3.1 | ...
'...
'This is a poor use of performance cache because all input parameters are different from one call to the next.
'Therefore no performance gains will be observed, and rather performance decreases are likely due to memory consumption.
table.filterByColumns(Array("Freq", "Risk"), stdLambda.Create("$1*$2 > 60",true))
'Data like
'| ID | Type | Status | ...
'|----|---------|----------| ...
'| 1 | Message | Archived | ...
'| 2 | Note | Active | ...
'| 3 | Message | Active | ...
'|----|---------|----------| ...
'Again this is a poor use of cache, as the table is so small. Performance cache is mainly only useful on large datasets.
myTable.filterByColumns(Array("Type", "Status"), stdLambda.Create("$1=""Message"" and $2=""Active""", true))
'Data like
'| ID | Type | Status | ...
'|----|---------|----------| ...
'| 1 | Message | Archived | ...
'| 2 | Note | Active | ...
'| 3 | Message | Active | ...
'| 4 | Message | Active | ...
'| 5 | Note | Active | ...
'...
'Unfortunately this is also a poor use of performance cache. $1 is the only argument and this will be a different row each time
'this is called. Therefore no performance benefits will be observed.
'Note: There is room to improve this behaviour at a later date.
myTable.filter(stdLambda.Create("$1.Type=""Message"" and $1.Status=""Active""", true))Use bSandboxExtras when you want strict control over the functions the user can call.
CreateMultiline(ByRef sEquation As Variant, Optional ByVal bUsePerformanceCache As Boolean = False, Optional ByVal bSandboxExtras As Boolean = False) As stdLambda
Creates and returns a stdLambda object which will execute the supplied equation body, when run.
stdLambda.CreateMultiline(Array( _
"if $1 = 0 then ""Test1""", _
"else if $1 = 1 then ""Test2""", _
"else ""Test3""" _
))See Create for usage of bUsePerformanceCache and bSandboxExtras
stdICallable is shared between stdLambda and stdCallback in stdVBA, however the intention is anyone can implement this same interface. Bare in mind that SendMessage will be implemented differently for each system.
'Call will call the passed function with param array
Public Function Run(ParamArray params() as variant) as variant: End Function
'Call function with supplied array of params
Public Function RunEx(ByVal params as variant) as variant: End Function
'Bind a parameter to the function
Public Function Bind(ParamArray params() as variant) as stdICallable: End Function
'Making late-bound calls to stdICallable members
'@protected
'@param {ByVal String} - Message to send
'@param {ByRef Boolean} - Whether the call was successful
'@param {ByVal Variant} - Any variant, typically parameters as an array. Passed along with the message.
'@returns {Variant} - Any return value.
Public Function SendMessage(ByVal sMessage as string, ByRef success as boolean, ByVal params as variant) as Variant: End FunctionIn this scenario 3 methods are implemented onto stdLambda:
obj - returns the object itself, can be used for casting to base type.
className - returns the object's class name.
bindGlobal - ICallable alias for bindGlobal.
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.
set lambda1 = stdLambda.Create("$1.name")
Debug.Print lambda1(ThisWorkbook)
set lambda2 = lambda1.bind(ThisWorkbook)
Debug.Print lambda2() 'same result as aboveTypically this is used when looping through a set of data, while relating back to something else. It is also significantly more optimal than continual recompiles. For instance:
For i = 1 to 10
'FAST - no need to continually recompile
stdArray.Create(1,2,3).map(stdLambda.Create("$2 / $1").bind(i)) 'note this is like "$1 / i"
'SLOW - Recompile on each loop
stdArray.Create(1,2,3).map(stdLambda.Create("$1 / " & i))
Next
'another example of usage:
set GetRecordsByDate = records.filter(stdLambda.Create("$2.Date = $1").bind(dt))Equivalent of Bind(), but with a passed array instead of a param array.
BindGlobal will bind a global function or variable onto the lambda it's called against. In order to bind a function vFunctionOrVariable must be an object which implements stdICallable.
Note: globals can be bound to
stdLambdabase class, e.g.stdLambda.bindGlobal("superGlobalCollection", new Collection), which will add the function to all newly generated lambdas.
Note:
stdCallbackcan be used to produce anstdICallablefrom a Module or Class function.
'Typical usage of bind as an enum-style type:
Sub test1Main()
Debug.Print test1(stdLambda.Create("Status.Red")) = 1 'True
Debug.Print test1(stdLambda.Create("Status.Green")) = 3 'True
End Sub
Function test1(lambda as stdLambda) as long
Static oStatus as object
if oStatus is nothing then
set oStatus = CreateObject("Scripting.Dictionary")
oStatus("Red") = 1
oStatus("Amber") = 2
oStatus("Green") = 3
end if
lambda.bindGlobal("Status", oStatus)
test1 = lambda.run()
End Function
'Usage to assign custom functions to a `stdLambda`.
Sub test2Main()
Debug.print test2(stdLambda.Create("addOne(21)")) = 22 'True
End Sub
Function test1(lambda as stdLambda) as long
Static addOne as stdICallable: if addOne is nothing then set addOne = stdLambda.Create("$1+1")
lambda.bindGlobal("addOne", addOne)
test2 = lambda.run()
End FunctionEvaluates the lambda expression.
See Run. Takes an array of parameters instead of a paramarray.