[Closed] Lambda Expressions in Maxscript – II
I think that finaly I’ve found a balanced solution between performance and usability.
It would be nice if you, experienced scripters, try it and give your feedback. Perhaps we arrive to something which is really worth using in our scripts.
Here’s the code. It’s just a global variable holding the struct for comparers and the ArrayQueryStruct to apply it to any array we want to inspect.
global MXSComparer =
(
struct MXSComparer
(
lambdaFN,
Point3Comparer =
(
struct point3Comparer
(
_zero = 0,
refPos = [0,0,0],
fn _Value x = distance x refPos ,
fn Equals x y = (x == y), -- Equality of Point3. NOT VALUE EQUALITY
fn SortASC x y =
(
d1 = distance x refPos
d2 = distance y refPos
case of
(
(d1 < d2):-1
(d1 > d2):1
default:0
)
),
fn SortDES x y =
(
d1 = distance x refPos
d2 = distance y refPos
case of
(
(d1 < d2):1
(d1 > d2):-1
default:0
)
),
on create do
(
)
)
point3Comparer()
),
IndexComparer =
(
struct IndexComparer -- Comparison based on equality of n'th element in subarray
(
_zero = 0,
Index = 1,
fn _Value x = x[Index],
fn Equals x y = -- all items in sub-array elements are equal to the ones of the given array of the same size. NOT VALUE EQUALITY
(
isEqual = true
for i = 1 to x.count while isEqual do isEqual = (x[i] == y[i])
isEqual
),
fn SortASC x y =
(
d1 = x[Index]
d2 = y[Index]
case of
(
(d1 < d2):-1
(d1 > d2):1
default:0
)
),
fn SortDES x y =
(
d1 = x[Index]
d2 = y[Index]
case of
(
(d1 < d2):1
(d1 > d2):-1
default:0
)
),
on create do
(
)
)
IndexComparer()
),
SumArrayComparer =
(
struct SumArrayComparer
(
_zero = 0,
fn _Value x = (sum = 0; for i in x do sum += i; sum), -- Sum of elements in subArray
fn Equals x y = -- all items in sub-array elements are equal to the ones of the given array of the same size. NOT VALUE EQUALITY
(
isEqual = true
for i = 1 to x.count while isEqual do isEqual = (x[i] == y[i])
isEqual
),
fn SortASC x y =
(
d1 = (sum = 0; for i in x do sum += i; sum)
d2 = (sum = 0; for i in y do sum += i; sum)
case of
(
(d1 < d2):-1
(d1 > d2):1
default:0
)
),
fn SortDES x y =
(
d1 = (sum = 0; for i in x do sum += i; sum)
d2 = (sum = 0; for i in y do sum += i; sum)
case of
(
(d1 < d2):1
(d1 > d2):-1
default:0
)
),
on create do
(
)
)
SumArrayComparer()
),
CaseInsensitive =
(
struct CaseInsensitive
(
_zero = "",
fn _Value x = toLower x,
fn Equals x y = ((stricmp x y) == 0),
fn SortASC x y =
(
d1 = toLower x
d2 = toLower y
case of
(
(d1 < d2):-1
(d1 > d2):1
default:0
)
),
fn SortDES x y =
(
d1 = toLower x
d2 = toLower y
case of
(
(d1 < d2):1
(d1 > d2):-1
default:0
)
),
on create do
(
)
)
CaseInsensitive()
),
NodeDistanceComparer =
(
struct NodeDistanceComparer
(
_zero = 0,
refPos = [0,0,0],
fn _Value x = distance x.pos refPos,
fn Equals x y = (x == y), -- Equality of Nodes. NOT VALUE EQUALITY
fn SortASC x y =
(
d1 = distance x.pos refPos
d2 = distance y.pos refPos
case of
(
(d1 < d2):-1
(d1 > d2):1
default:0
)
),
fn SortDES x y =
(
d1 = distance x.pos refPos
d2 = distance y.pos refPos
case of
(
(d1 < d2):1
(d1 > d2):-1
default:0
)
),
on create do
(
)
)
NodeDistanceComparer()
),
NodeSizeComparer =
(
struct NodeSizeComparer
(
_zero = 0,
fn _Value x = (bb = nodeGetBoundingBox x x.transform; D = distance bb[1] bb[2]; Vol = D*D*D/3./(sqrt 3); side = Vol^(1/3.)),
fn Equals x y = (x == y), -- Equality of Nodes. NOT VALUE EQUALITY
fn SortASC x y =
(
d1 = (bb = nodeGetBoundingBox x x.transform; D = distance bb[1] bb[2]; Vol = D*D*D/3./(sqrt 3); side = Vol^(1/3.))
d2 = (bb = nodeGetBoundingBox y y.transform; D = distance bb[1] bb[2]; Vol = D*D*D/3./(sqrt 3); side = Vol^(1/3.))
case of
(
(d1 < d2):-1
(d1 > d2):1
default:0
)
),
fn SortDES x y =
(
d1 = (bb = nodeGetBoundingBox x x.transform; D = distance bb[1] bb[2]; Vol = D*D*D/3./(sqrt 3); side = Vol^(1/3.))
d2 = (bb = nodeGetBoundingBox y y.transform; D = distance bb[1] bb[2]; Vol = D*D*D/3./(sqrt 3); side = Vol^(1/3.))
case of
(
(d1 < d2):1
(d1 > d2):-1
default:0
)
),
on create do
(
)
)
NodeSizeComparer()
),
on create do
(
)
)
MXSComparer()
)
struct ArrayQuery
(
mainArray,
ResultArray,
useComp = false,
comparerParam = "",
Comparer,
fn useComparer use =
(
useComp = use
comparerParam = if useComp then " Comparer:Comparer" else ""
),
fn setComparer comparerName =
(
comparerName = toLower comparerName
Comparer = case comparerName of
(
"point3comparer": ::MXSComparer.Point3Comparer
"indexcomparer": ::MXSComparer.IndexComparer
"sumarraycomparer": ::MXSComparer.SumArrayComparer
"caseinsensitive": ::MXSComparer.CaseInsensitive
"nodedistancecomparer": ::MXSComparer.NodeDistanceComparer
default: (print "Unknown Comparer"; undefined)
)
),
-- Query Functions
fn _where LA =
(
LA = LA as string
assignPos = findString LA "=>"
fnExpression = (substring LA 1 (assignPos - 1)) + comparerParam + " = " + (substring LA (assignPos+2) -1)
LA = execute ("::MXSComparer.lambdaFN = fn GlobalLambdaFN " + fnExpression)
if useComp then
(
ResultArray = for item in ResultArray where (LA item Comparer:Comparer) collect item
)
else
(
ResultArray = for item in ResultArray where (LA item) collect item
)
return (this)
),
fn Change LA =
(
LA = LA as string
assignPos = findString LA "=>"
fnExpression = (substring LA 1 (assignPos - 1)) + " = " + (substring LA (assignPos+2) -1)
LA = execute ("::MXSComparer.lambdaFN = fn GlobalLambdaFN " + fnExpression)
ResultArray = for item in ResultArray collect (LA item)
return (this)
),
fn GroupBy LA =
(
LA = LA as string
assignPos = findString LA "=>"
fnExpression = (substring LA 1 (assignPos - 1)) + comparerParam + " = " + (substring LA (assignPos+2) -1)
LA = execute ("::MXSComparer.lambdaFN = fn GlobalLambdaFN " + fnExpression)
if useComp then
(
tempResult = #()
notFound = #{1..ResultArray.count}
count = 0
for i = 1 to ResultArray.count where notFound[i] do
(
count += 1
append tempResult #()
append tempResult[count] ResultArray[i]
check = LA ResultArray[i] Comparer:Comparer
for j = i+1 to ResultArray.count where (notFound[j] and ((LA ResultArray[j] Comparer:Comparer) == check)) do
(
notFound[j] = false
append tempResult[count] ResultArray[j]
)
)
ResultArray = deepcopy tempResult
)
else
(
tempResult = #()
notFound = #{1..ResultArray.count}
count = 0
for i = 1 to ResultArray.count where notFound[i] do
(
count += 1
append tempResult #()
append tempResult[count] ResultArray[i]
check = LA ResultArray[i]
for j = i+1 to ResultArray.count where (notFound[j] and ((LA ResultArray[j]) == check)) do
(
notFound[j] = false
append tempResult[count] ResultArray[j]
)
)
ResultArray = deepcopy tempResult
)
return (this)
),
fn maxObjectQuery obj LA =
(
LA = LA as string
assignPos = findString LA "=>"
fnExpression = (substring LA 1 (assignPos - 1)) + " = " + (substring LA (assignPos+2) -1)
LA = execute ("::MXSComparer.lambdaFN = fn GlobalLambdaFN obj " + fnExpression)
ResultArray = for item in ResultArray where (LA obj item) collect (item)
return (this)
),
-- Filter Functions
fn MakeUnique =
(
if useComp then
(
tempResult = #()
found = #{1..ResultArray.count}
for i = 1 to ResultArray.count where found[i] do
(
append tempResult ResultArray[i]
for j = i+1 to ResultArray.count where found[j] do
(
if Comparer.Equals ResultArray[i] ResultArray[j] do found[j] = false
)
)
ResultArray = deepcopy tempResult
)
else
(
ResultArray = makeUniqueArray ResultArray
)
return (this)
),
fn Intersect otherArray =
(
if useComp then
(
tempResult = #()
for item in ResultArray do
(
notfound = true
for item2 in otherArray while notfound do
(
if (Comparer.Equals item item2) do (notfound = false; append tempResult item)
)
)
ResultArray = deepcopy tempResult
)
else
(
ResultArray = for item in ResultArray where (findItem otherArray item != 0) collect item
)
return (this)
),
fn Except otherArray =
(
if useComp then
(
tempResult = #()
for item in ResultArray do
(
notfound = true
for item2 in otherArray while notfound do
(
if (Comparer.Equals item item2) do (notfound = false)
)
if notfound do append tempResult item
)
ResultArray = deepcopy tempResult
)
else
(
ResultArray = for item in ResultArray where (findItem otherArray item == 0) collect item
)
return (this)
),
fn Union otherArray =
(
if useComp then
(
ResultArray = (ResultArray + otherArray)
MakeUnique()
)
else
(
ResultArray = makeUniqueArray (ResultArray + otherArray)
)
return (this)
),
-- Final Functions
fn Select =
(
Result = deepCopy ResultArray
ResultArray = deepCopy mainArray
return Result
),
fn Count =
(
Result = ResultArray.count
ResultArray = deepCopy mainArray
return Result
),
fn Sum =
(
if useComp then
(
suma = Comparer._zero
for item in ResultArray do (suma += Comparer._Value item)
)
else
(
arrayClass = classof ResultArray[1]
suma = if arrayClass == string then "" else 0
for item in ResultArray do (suma += item)
)
ResultArray = deepCopy mainArray
return suma
),
fn Average =
(
numItems = ResultArray.count
result = (sum() as float)/numItems
return result
),
fn SortASC =
(
if useComp then
(
qsort ResultArray Comparer.sortASC
Result = deepCopy ResultArray
)
else
(
sort ResultArray
Result = deepCopy ResultArray
)
ResultArray = deepCopy mainArray
return Result
),
fn SortDES =
(
if useComp then
(
qsort ResultArray Comparer.sortDES
Result = deepCopy ResultArray
)
else
(
fn SortDescending x y =
(
case of
(
(x < y):1
(x > y):-1
default:0
)
)
qsort ResultArray SortDescending
Result = deepCopy ResultArray
)
ResultArray = deepCopy mainArray
return Result
),
fn SortByIndex Index:1 Dir:"ASC"= -- Sort multiArray by index. Dir: "ASC" ascending; "DES" descending
(
Dir = toLower Dir
direction = if Dir == "des" then -1 else 1
BaseIndex = Index
fn Sorting x y BaseIndex:BaseIndex direction:direction =
(
case of
(
(x[BaseIndex] < y[BaseIndex]): (-1 * direction)
(x[BaseIndex] > y[BaseIndex]): (direction)
default:0
)
)
qsort ResultArray Sorting BaseIndex:BaseIndex direction:direction
Result = deepCopy ResultArray
ResultArray = deepCopy mainArray
return Result
),
fn FindAllIndex LA = -- 'index' == reserved word for array index
(
LA = LA as string
assignPos = findString LA "=>"
fnExpression = (substring LA 1 (assignPos - 1)) + comparerParam + " = " + (substring LA (assignPos+2) -1)
LA = execute ("::MXSComparer.lambdaFN = fn GlobalLambdaFN index " + fnExpression)
if useComp then
(
ResultIndex = for id = 1 to ResultArray.count where (LA id ResultArray[id] Comparer:Comparer) collect id
)
else
(
ResultIndex = for id = 1 to ResultArray.count where (LA id ResultArray[id]) collect id
)
ResultArray = deepCopy mainArray
return ResultIndex
),
fn FindIndex LA = -- 'index' == reserved word for array index
(
LA = LA as string
assignPos = findString LA "=>"
fnExpression = (substring LA 1 (assignPos - 1)) + comparerParam + " = " + (substring LA (assignPos+2) -1)
LA = execute ("::MXSComparer.lambdaFN = fn GlobalLambdaFN index " + fnExpression)
ResultIndex = -1
if useComp then
(
for id = 1 to ResultArray.count where (LA id ResultArray[id] Comparer:Comparer) while (ResultIndex == -1) do (ResultIndex = id)
)
else
(
for id = 1 to ResultArray.count where (LA id ResultArray[id]) while (ResultIndex == -1) do (ResultIndex = id)
)
ResultArray = deepCopy mainArray
return ResultIndex
),
on create do
(
ResultArray = deepCopy mainArray
)
)
Reserved for using explanation
User’s guide:
The ‘ArrayQuery’ struct purpose is to query arrays and multiarrays using predefined functions through a kind of ‘lambda expressions’.
The general syntax is:
<queryStruct> = ArrayQuery <array>
=====> Assigns the array to query to an ArrayQuery struct
The predefined query functions are divided in three groups:
- Query Functions: functions that query the array through a lambda expression (_Where, Change, GroupBy, maxObjectQuery). The result of this query is a value held in the QueryStruct, so you should apply a ‘Final Function’ to use the result.
- Filter Functions: these functions have the same behaviour than ‘Query Functions’, but they don’t use lambda expressions (MakeUnique, Intersect, Except, Union).
- Final Functions: these functions give a result of your query that you can store in any variable (Select, Count, Sum, Average, SortASC, SortDES, SortByIndex, FindAllIndex, FindIndex). Some of these functions use a lambda expression to work (FindAllIndex, FindIndex).
The general syntax is:
<Value> = <queryStruct>.[B]FinalFunction/B
=====> Apply a final function to the array
<Value> = (<queryStruct>.QueryFunction(#‘lambda expression’)).[B]FinalFunction/B
=====> Apply a query function to the array and then get a result through a final function
<Value> = (<queryStruct>.[B]FilterFunction/B).[B]FinalFunction/B
=====> Apply a filter function to the array and then get a result through a final function
You can apply as many query and filter functions as needed to a query result, always adding parenthesis to the prior query:
<Value> = ((<queryStruct>.QueryFunction(#‘lambda expression’)).[B]FilterFunction/B).[B]FinalFunction/B
<Value> = (((<queryStruct>.QueryFunction(#‘lambda expression’)).[B]FilterFunction/B).QueryFunction(#‘lambda expression’)).[B]FinalFunction/B
Lambda Expressions are a name (#’<name>) or a string (“string”) with next syntax:
<var> => <var expression>
where <var> is a variable name designating the array item and <var expression> is a maxscript expression using the <var> variable.
FindAllIndex and FindIndex final functions can use the reserved word ‘index’ in the <var expression> that points to the index of the <var> element in the array.
You can use a predefined Comparer in your query (CaseInsensitive, Point3Comparer, NodeDistanceComparer, SumArrayComparer, IndexComparer). In this case, all query functions will work with defined values and equalities in the selected Comparer.
To do it, the syntax is:
<queryStruct>.[B]useComparer/B
<queryStruct>.setComparer “ComparerName”
You can use Comparer definitions in lambda expresions using the Comparer struct, for example:
#‘x => Comparer._Value x > 10’ to query for values greater than 10 using ‘Comparer Value’
#‘x => Comparer.Equals x 10’ to query for values equal to 10 using ‘Comparer Value’
And set the Comparer parameters via:
<queryStruct>.Comparer.parameterName = <newValue>
All these parameters are defined at the top of each Comparer Struct Definition.
Reserved for general arrays query examples
QUERY FUNCTIONS
-
_Where(#‘lambda expression’): general purpose query function. Lambda expression must evaluate to <boolean>.
Ex.: (QArray._Where(#’node => node.radius > 15.0)).Select() -
Change(#‘lambda expression’): function to make changes to the query array. It doesn’t admit Comparer use. Lambda expression can evaluate to any <value>
Ex.1: (QArray.Change(#’x => x^2)).Select() ____ (returns all elements in the array powered by 2)
Ex.2: (QArray.Change(#’subArrayItem => subArrayItem[2])).Select() ____ (returns a new array with only 2nd element of each subArrayItem in the query array) -
GroupBy(#‘lambda expression’): function to make groups based on the Lambda expression, that can evaluate to any <value>, included boolean. Each group is stored as a subArray of the result Array.
Ex.1: (QArray.GroupBy(#’item => item[3]>7)).Select() ____ (returns two subgroups)
Ex.2: (QArray.GroupBy(#’item => item[3])).Select() ____ (returns as many subgroups as different values exist of 3rd element in subArrays of the query array)
Ex.3: (QArray.GroupBy(#’item => item.radius)).Select() ____ (returns subgroups of elements with the same radius) -
maxObjectQuery <maxObject> (#‘lambda expression’): function to query the array using a maxObject in the Lambda expression, that must evaluate to <boolean>. There’s a reserved word ‘obj’ for the maxObject in the Lambda expression. This function doesn’t allow Comparer use.
Ex.: (QArray.maxObjectQuery sp (#‘vert => (getvert obj vert).z < 0’)).select() ____ (returns all items for whose the evaluation of lambda expression is true (where ‘sp’ is the maxObject passed to the lambda function)
Example 1: ‘GroupBy’
Group a cloud of Point3 by their Y coordinate
(
myArray = for i = 1 to 1000 collect [random 0 100, random 0 100, random 0 100]
QArray = ArrayQuery myArray
QArray.useComparer(false)
t1 = timestamp()
groupedArray = (QArray.GroupBy(#'coord => coord.Y')).select()
t2 = timestamp()
format "Result = %
" groupedArray
format "Result.count= %
" groupedArray.count -- 101 groups
format "time1= %ms
" (t2-t1) -- 120ms for 1000 items
)
Reserved for arrays query using Comparer examples
Example 1: ‘NodeDistanceComparer’
We want to select all nodes that have their coord X positive and are at a distance less than 50 units from a target node:
(
delete objects
for i = 1 to 2000 do point prefix:"aaa" pos:[random -100 100, random -100 100, random -100 100]
nodeArray = objects as array
refNode = sphere name:"refNode" radius:50 pos:[random -100 100, random -100 100, random -100 100]
t1 = timestamp()
Qnodes = ArrayQuery nodeArray
Qnodes.useComparer(true)
Qnodes.setComparer "NodeDistanceComparer"
Qnodes.Comparer.refPos = refNode.pos
theNodes =((Qnodes._Where(#'node => node.pos.x >= 0'))._Where(#'dist => Comparer._value dist < 50')).select()
t2 = timestamp()
select theNodes
format "NODE DISTANCE COMPARER
"
format "theNodes = %
" theNodes
format "theNodes.count= %
" theNodes.count
format "time= %ms
" (t2 - t1) -- 9ms for 2000 nodes
)
Example 2: ‘SumArrayComparer’
(
myArray = for i = 1 to 1000 collect #(random 0 10, random 0 10, random 0 10)
QArray = ArrayQuery myArray
QArray.useComparer(true)
QArray.setComparer "SumArrayComparer"
t1 = timestamp()
kk = (QArray._where(#'x => Comparer._Value x < 15')).sortDES()
t2 = timestamp()
format "Sort Descending elements whose Value is Less than 15 = %
" kk
format "time1= %ms
" (t2-t1) -- 22ms for 1000 elements multiArray
)
Reserved for performance tests
test 1: ‘GroupBy’ (+50% time)
(
myArray = for i = 1 to 1000 collect [random 0 100, random 0 100, random 0 100]
QArray = ArrayQuery myArray
QArray.useComparer(false)
t1 = timestamp()
groupedArray = (QArray.GroupBy(#'coord => coord.Y')).select()
t2 = timestamp()
format "Result = %
" groupedArray
format "Result.count= %
" groupedArray.count -- 101 groups
format "time1= %ms
" (t2-t1) -- 120ms for 1000 items
t1 = timestamp()
groupedArray = #()
notFound = #{1..myArray.count}
count = 0
for i = 1 to myArray.count where notFound[i] do
(
count += 1
append groupedArray #()
append groupedArray[count] myArray[i]
check = myArray[i].Y
for j = i+1 to myArray.count where (notFound[j] and ((myArray[j].Y) == check)) do
(
notFound[j] = false
append groupedArray[count] myArray[j]
)
)
t2 = timestamp()
format "Result = %
" groupedArray
format "Result.count= %
" groupedArray.count -- 101 groups
format "time1= %ms
" (t2-t1) -- 81ms for 1000 items
)
120ms vs 81ms (148% time) for 1000 items
253ms vs 168ms (151% time) for 2000 items
Reserved for updates
09/01/2017:
- added Query Function ‘GroupBy’: create multiArray of groups following lambda expression criteria.
- added Final Function ‘Average’: returns the mean of the query
- added Final Function ‘SortByIndex’: returns the sorted array based on n’th element’s value
10/01/2017: - added Query Function ‘maxObjectQuery’: allows to use a <MaxObject> for the query
I wonder whats the purpose behind of doing this ? I dont see this will speed up workflow of doing thing in ms-ing, a riddle scripting, or is this to make me got headache,maybe?
Thanks for posting, k4noe.
The purpose is, effectively, speed up workflow. Have you tried it? As functions are predefined, you can save time of coding. And performance is good enough for most purposes.
Of course it’s a new set of instructions that we have to learn and explore. That can be a little headache if you’ve never worked with lambda expressions. But it can finally get useful. I’m sure.
i don’t see a big reason to do ‘lambda style’ mxs coding, but if i would do:
that’s how i’d do it:
fn _one_arg_execute act a = act a
_one_arg_execute (fn'' a = "_" + a as string) 4
fn _two_arg_execute act a b = act a b
_two_arg_execute (mapped fn'' i a = format a i) #(1,2,3) ">> %
"
it looks pretty clear for me, and doesn’t need ‘pre-compilation’ or ‘parsing’
the most interesting implementation for ‘lambda’ could be any ‘mapped’ tasks:
fn _three_arg_execute act a b c = (c = act a b c)
(
delete objects
sp = converttomesh (geosphere())
ii = #()
_three_arg_execute (mapped fn'' i mesh list = (if (getvert mesh i).z < 0 do append list i; list)) (#{1..sp.numverts} as array) sp ii
sp.selectedverts = ii
)
I absolutely agree with you. And I haven’t implemented it.
With actual code, we could do:
(
delete objects
sp = converttomesh (geosphere())
QArray = ArrayQuery (#{1..sp.numverts} as array)
ii = (QArray._Where(#'vert => (getvert $GeoSphere001 vert).z < 0')).select()
sp.selectedverts = ii
)
or…
(
delete objects
sp = converttomesh (geosphere())
QArray = ArrayQuery (for v = 1 to sp.numverts collect (getvert sp v))
ii = QArray.FindAllIndex(#'vert => vert.z < 0')
sp.selectedverts = ii
)
or…
(
delete objects
sp = converttomesh (geosphere segments:20)
QArray = ArrayQuery #(sp)
ii = ((QArray.Change(#'node => for v = 1 to node.numverts collect (getvert node v)')).Change(#'verts => for v= 1 to verts.count where verts[v].z<0 collect v')).Select()
sp.selectedverts = ii[1]
)
The problem I see with your proposal is the ‘query of query’. Imagine that, it’s not pretty clear:
b=#(); _two_arg_execute (mapped fn'' i a = format a i) (_two_arg_execute (mapped fn'2' c b= append b (c*2)) #(#(1,2,3), #(4,5,6), #(7,8,9)) b; b) ">> %
"
Just for test, with a geoSphere with 20 segments (4002 verts):
- Your mapped function: 9ms
- First query method: 18ms
- Second query method: 13ms
- Third query method: 9ms