# Introduction
OScript scripts behave like other data types. Like an integer, string, or list, a script can be assigned to a variable and passed as a parameter to other functions. For example:
// Assign a script to a variable
Script s = $MyModule.SomeGlobal.SomeScript
// Pass the script to another function
$MyModule.Utils.SomeFunction(s)
2
3
4
5
A Script
variable even has a type:
echo( Type(s) == ScriptType )
> true
2
Programming languages that behave this way are known as having first-class functions (opens new window) (or "scripts" in our case), which means the language treats functions just like any other data type.
Scripts can also be executed:
// Execute the script with some parameters
Dynamic rtnVal = s(parm1, ..., parmN)
2
This is a powerful feature of OScript, which permits some novel solutions to certain problems. It surprises me that it's almost never used.
Passing around scripts this way comes with a few caveats. In this blog post I'll discuss what needs to be kept in mind, provide some examples, and introduce a way to write simple anonymous functions in OScript.
Just a word about terminology. I interchange the words "function" and "script", which for the purpose of this post are the same thing.
# The problem with "this"
Almost all scripts in Content Server are defined on an object. The object can be thought of as the script "owner", which is the value of this
when the script is normally executed. This setup allows you to access other features and functions on the object with the dot-notation (e.g., .fFeatureName
or .someFunction()
, which is the same as this.fFeatureName
or this.someFunction()
).
Things get tricky when you start assigning scripts to variables, passing them into functions, and executing them in a context different to where it was defined. When you do this the value of this
becomes the object from where the script was called, and not where the script was defined. This causes problems if the script tries to access any feature or script on this
(using the dot-notation) that doesn't exist in the new context. It causes a runtime error and stack trace.
Let's look at an example. Say we have the following two functions defined in $MyModule.MathUtils
:
// Square an integer
function square(Integer myInteger)
return myInteger * myInteger
end
// Square an integer (by referencing square) and add one
function Integer squarePlusOne(Integer myInteger)
return .square(myInteger) + 1
end
2
3
4
5
6
7
8
9
From anywhere in the system we could do the following:
Script myScript = $MyModule.MathUtils.square
echo( myScript(5) )
> 25
2
3
4
This works because the square()
function doesn't reference a feature or script on this
.
However, we run into problems if we try the same with squarePlusOne
. The function references the square
function, which is not in context once the script is assigned to a variable and executed outside of $MyModule.MathUtils
:
Script myScript = $MyModule.MathUtils.squarePlusOne
// stack trace on the call to square()
echo( myScript(5) )
2
3
4
Is there a way to handle this?
# Controlling the value of "this"
For years I wondered if there was a way to control the value of this
when executing a script (e.g., like the call
(opens new window) function in JavaScript). I touched on the subject in my last blog post, but didn't provide a way to explicitly set the value. I dismissed it as not being possible, but recently stumbled across a way to do it. I assume the syntax has long been forgotten since I've only ever seen it used once and can't find it documented anywhere. Here's how it works:
Say you have a Script
variable named myScript
, and wish to execute it with this
having the value thisArg
. The syntax is:
thisArg.(myScript)(arg1, ..., argN)
Yes, it's that simple.
What's interesting is that thisArg
can be of any data type, even another script! In other words, you could also do this:
Integer i = 5
// Execute myScript with "this" having the value 5
i.(myScript)(arg1, ..., argN)
2
3
4
Strange! The syntax means we can rewrite our squarePlusOne
example as:
Script myScript = $MyModule.MathUtils.squarePlusOne
Integer myValue = $MyModule.MathUtils.(myScript)(5)
2
3
The example isn't practical, but demonstrates how this
can be explicitly set when executing a script. Generally speaking, we could replace $MyModule.MathUtils
in the second line with any object in the system as long as it contains a square()
function that accepts and returns an integer. For example:
Integer myValue = $MyOtherModule.BetterMathUtils.(myScript)(5)
So where is this useful?
# Map Function
A map
function applies a function to each element of a collection (a List
or RecArray
in our case), and returns the results as a list. OScript doesn't provide such a function, so I added one to RHCore ($RHCore.ListUtils.map
). For example, we can square a list of integers by using map
and the square
function we defined earlier:
List squaredValues = $RHCore.ListUtils.map({1,2,5,6}, $MyModule.Math.square)
echo(squaredValues)
> {1,4,25,36}
2
3
4
It's a powerful way to operate on each element of a list without having to setup a loop each time. Unfortunately, we run into problems if we try the same with the squarePlusOne
function. By passing $MyModule.Math.squarePlusOne
into the map
function we change the value of this
, which causes the square()
call to fail.
For this reason the map
function accepts a third parameter to control the this
value on the script. That is:
List newValues = $RHCore.ListUtils.map({1,2,5,6}, $MyModule.Math.squarePlusOne, $MyOtherModule.BetterMathUtils)
echo(newValues)
> {2,5,26,37}
2
3
4
Neat, huh?
# Revisiting the making of "super" calls
In Part IX of this blog series I discussed a way to make super()
calls in OScript. You may recall the problem of context when calling super.funcName()
directly:
function Dynamic funcName(Dynamic arg1, Dynamic arg2, ..., Dynamic argN)
Dynamic rtnVal = super.funcName(arg1, arg2, ..., argN)
// do other stuff
return rtnVal
end
2
3
4
5
6
7
8
The problem is that super.funcName()
runs in the context of the ancestor and not the class that called it. However, this can be fixed with the syntax we just discussed:
function Dynamic funcName(Dynamic arg1, Dynamic arg2, ..., Dynamic argN)
// the "this" keyword is implied before the period
Dynamic rtnVal = .(super.funcName)(arg1, arg2, ..., argN)
// do other stuff
return rtnVal
end
2
3
4
5
6
7
8
9
I wish I had known about this before! But as an aside, this hasn't deterred me from needing my own super
function (since I need to support the Frame
data type).
# Anonymous Functions
While researching this blog post I started wondering whether anonymous functions could be supported in OScript. Since scripts are first-class functions, you would sort of expect it to be possible to define a script on the fly just as you would with a string or integer literal.
Anonymous functions are not natively supported in OScript, but I realized something simple could be written in OScript. With a few lines of code I put together a lambda
function, which is motivated by the lambda function in Python (opens new window).
Here's how it works: The lambda
function in RHCore accepts a list of parameter names (as a list or comma-separated string) and a single-line expression. For example:
Script square = $RHCore.OScriptUtils.lambda('x', 'x*x')
This returns a compiled script that looks like this:
function Dynamic lambda(Dynamic x)
return x*x
end
2
3
We can use it directly:
Integer myValue = square(5)
echo( myValue )
> 25
2
3
4
We can also use it with the map
function to square a list of integers:
List newValues = $RHCore.ListUtils.map({1,2,5,6}, square)
Or, in an even more compact syntax:
List newValues = $RHCore.ListUtils.map({1,2,5,6}, $RHCore.OScriptUtils.lambda('x', 'x*x'))
Neat, huh?
Let's look at a more relatable example. In RHCore is a filter
function that can be used to filter the items in a List
or RecArray
. The function accepts a collection and script as parameters, applies the script to each element, and keeps any item where the return value of the script is true
.
So let's suppose we had a RecArray
that came from querying the WebNodes
view. We now want to filter it in memory to keep the folders:
// Get our nodeRecs from somewhere
RecArray nodeRecs = ...
// Create a lambda function to filter the records
Script filterFolders = $RHCore.OScriptUtils.lambda('rec', 'rec.SUBTYPE == $TypeFolder')
// Apply the filter
RecArray foldersOnly = $RHCore.ListUtils.filter( nodeRecs, filterFolders )
2
3
4
5
6
7
8
Or, in a single line:
RecArray foldersOnly = $RHCore.ListUtils.filter( nodeRecs, $RHCore.OScriptUtils.lambda('rec', 'rec.SUBTYPE == $TypeFolder') )
Who would have thought this was possible with OScript?
# Wrapping Up
OScript is clearly more advanced than most people give it credit. Who would ever have thought this style of programming was possible? I'm using these tools and conventions more and more in my projects, and find it creates much more robustness with less repetition. It's what I like.
Questions or comments about what you read? Please leave a comment!