Part X - Advanced Script Handling and Anonymous Functions in OpenText Content Server

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)
1
2
3
4
5

A Script variable even has a type:

echo( Type(s) == ScriptType )
> true
1
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)
1
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
1
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
1
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) )
1
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)
1

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)
1
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)
1
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)
1

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}
1
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 executing script. That is:

List newValues = $RHCore.ListUtils.map({1,2,5,6}, $MyModule.Math.squarePlusOne, $MyOtherModule.BetterMathUtils)

echo(newValues)
> {2,5,26,37}
1
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
1
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
1
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')
1

This returns a compiled script that looks like this:

function Dynamic lambda(Dynamic x)
    return x*x
end
1
2
3

We can use it directly:

Integer myValue = square(5)

echo( myValue )
> 25
1
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)
1

Or, in an even more compact syntax:

List newValues = $RHCore.ListUtils.map({1,2,5,6}, $RHCore.OScriptUtils.lambda('x', 'x*x'))
1

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 )
1
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') )
1

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!