Functional Metaprogramming

Functional metaprogramming allows for the dynamic creation and manipulation of function definitions, enabling functions to be defined, modified, or executed during runtime. This approach simplifies the process of defining functions on the fly.

Obtaining Function Definitions Dynamically

In DolphinDB, a function definition is represented by the data type FUNCTIONDEF. You can use the built-in function funcByName to dynamically retrieve a function definition based on the its name.

For example, a variable name contains function name “sin”. You can use funcByName to obtain function definition of sin based on its name stored in name. Then you can pass v to the function for calculation.
name = `sin
v = 1..10
funcByName(name)(v)

// output: [0.8415,0.9093,0.1411,-0.75688,-0.9589,-0.2794,0.657,0.9894,0.4121,-0.5440]
Note: If a function is defined within a module, you can include the module name as a prefix, separated by the namespace symbol ::.

There are scenarios where you might need to work with anonymous functions (including lambda expressions). To handle these cases, you can pass the function definition as a string variable to parseExpr. ParseExpr returns the metacode, which can be evaluated with eval.

For example:
funcDef = "x->1 + x + x*x"
parseExpr(funcDef).eval().call(2)
//output: 7

Calling Dynamically-Generated Functions

DolphinDB offers three methods for invoking dynamic functions: the higher-order function call, the operator (), and the at function.
f = parseExpr("{x,y->(x - y)/(x + y)}").eval()

// method 1: using the call function
call(f, 3.0, 2.0)

// method 2: using operator ()
f(3.0, 2.0)

// method 3: using the at function
at(f, (3.0, 2.0))

The first two methods, call and (), are similar in that they accept a variable number of arguments, requiring that each argument of the target function be individually provided during invocation.

The at function, however, always takes exactly two arguments: the function definition and the argument(s) needed for the function. This fixed two-argument syntax offers advantages in dynamic invocation scenarios, reducing complexity and potential errors. When calling a function requiring multiple arguments, the second argument of the at function must be a tuple, with each element representing one of the function's inputs.

A special case arises when the function being called is a unary function that itself takes a tuple as its single input. In this case, to use the at function correctly, you must use the enlist function to wrap the input tuple as a new tuple with the original tuple as its sole element. Without this step, the system would interpret the elements of the original tuple as separate arguments, leading to an error.

For example:
f = {x->x.head()\x.tail()}
x = (1,2,3)
// Reports error: The function [] expects 1 argument(s), but the actual number of arguments is: 3
at(f, x)
// correct syntax
at(f, enlist x)

Generating Function Calls Dynamically

While the dynamic function calls discussed earlier execute immediately and return a value, there are scenarios where we need to create code containing dynamic function calls for later execution, such as defining filtering conditions with functions in SQL queries.

To facilitate this, DolphinDB provides two specialized functions: makeCall and makeUnifiedCall. These functions generate code for function calls that can be used in lazy execution.

The key difference between them mirrors the distinction between the call and at functions: makeCall requires you to input arguments based on the number needed by the corresponding function. makeUnifiedCall requires a fixed number of arguments. If the function requires multiple arguments, they are encapsulated in a tuple.

For example, there is a lambda function that takes two arguments, x and y. We want to use this function with two columns from a table, “qty1” and “qty2”, as input arguments.
  • Using makeCall: Use sqlCol to generate metacode for each input column.
  • Using makeUnifiedCall: a. Manually create a tuple with each element generated with sqlCol, or b. use sqlTuple to generate metacode with a tuple expression.
f = parseExpr("{x,y->(x - y)/(x + y)}").eval()
makeCall(f, sqlCol("qty1"), sqlCol("qty2"))
makeUnifiedCall(f, (sqlCol("qty1"), sqlCol("qty2")))
makeUnifiedCall(f, sqlTuple(`qty1`qty2))
Further, the result of makeCall/makeUnifiedCall can be passed as the parameter select of function sql to generate SQL dynamically. The selected columns will be processed with f.
f = parseExpr("{x,y->(x-y)/(x+y)}").eval()
t = table(1.0 2.0 3.0 as qty1, 1.0 3.0 7.0 as qty2)
sql(select=makeCall(f, sqlCol("qty1"), sqlCol("qty2")), from=t).eval()
sql(select=makeUnifiedCall(f, (sqlCol("qty1"), sqlCol("qty2"))), from=t).eval()
sql(select=makeUnifiedCall(f, sqlTuple(`qty1`qty2)), from=t).eval()
Executing line 3, 4, 5 can obtain the same result:
_qty1
0
-0.2
-0.4