Code as an argument in Caché ObjectScript
The InterSystems Caché ObjectScript (COS) language is developing every year, new commands and functionality are added to it . Unfortunately, at the moment, routines in COS are not objects of the first class , that is, a routine (function, method) cannot be passed as a parameter to a routine or returned from a routine.
However, there are ways to mitigate these limitations.
Under the cat, consider several options for passing code as an argument to a subroutine.
Suppose there are two of the following methods:
ClassMethod AllPersonsWithA ()
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare ("select ID from Sample.Person where substr (name, 1,1) = 'A '")
do rs.Execute ()
while rs.Next () {
set p = ## class (Sample.Person).% OpenId (rs.Get ("ID"))
set p.Office = "Moscow"
write p.Name, "", p.SSN !
kill p
}
kill rs
}
ClassMethod AllCompaniesWithO ()
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare ("select ID from Sample.Company where substr (name, 1,1) = 'O '")
do rs.Execute ()
while rs.Next () {
set p = ## class (Sample.Company).% OpenId (rs.Get (" ID "))
set p.Name =" OOO "_p. Name
write p.Name ,!
kill p
}
kill rs
}
To switch to the new version of dynamic SQL -% SQL.Statement we have to change the code in two places. If at us% ResultSet was used in ten places, would change in ten.
We rewrite these two methods as follows.
Add a method to handle a specific instance of Sample.Person.
ClassMethod ProcessPerson (p As Sample.Person)
{
set p.Office = "Moscow"
write p.Name, "", p.SSN ,!
}
With this command, we replace the AllPersonsWithA method:
do ..OpenAndProcess ("select ID from Sample.Person where substr (name, 1,1) = 'A'", "Sample.Person", "ProcessPerson")
Here, the first argument is the query, the second is the name of the class whose instances will be processed, the third is the name of the class method that needs to be called for each row of the query result.
For companies, the processing method will look like this:
ClassMethod ProcessCompany (c As Sample.Company)
{
set c.Name = "OOO" _c.Name
write c.Name ,!
}
We will
call the following command: do ..OpenAndProcess ("select ID from Sample.Company where substr (name, 1,1) = 'O'", "Sample.Company", "ProcessCompany")
Now, in fact, the OpenAndProcess method itself :
ClassMethod OpenAndProcess (query As% String, className As% String, callback As% String)
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare (query)
do rs.Execute ()
while rs.Next () {
set p = $ classmethod (className, "% OpenId", rs.Get ("ID"))
do $ classmethod ($ classname () , callback, p);
kill p
}
kill rs
}
The function $ classmethod (class, method, arg1, arg2, ...) calls the class method called method from the class class and passes arg1, arg2, etc. to it.
Now, working with% ResultSet has been moved to a separate method, what is being done there does not interest anyone.
Obviously, the OpenAndProcess method can be fixed so that it does not call the method from the current, but from an arbitrary class and passes an arbitrary number of parameters to callback.
If the code for callback is very small, you can use the $ xecute function, which is a kind of analogue of anonymous functions. The OpenAndProcess method in this case would look like:
ClassMethod OpenAndProcess (query As% String, className As% String, callback As% String)
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare (query)
do rs.Execute ()
while rs.Next () {
set p = $ classmethod (className, "% OpenId", rs.Get ("ID"))
set res = $ xecute (callback, p)
kill p
}
kill rs
}
And the processing would not be enclosed in a class method, but in a string.
set query = "select ID from Sample.Person where substr (name, 1,1) = 'A'"
do ..OpenAndProcess (query, "Sample.Person", "(p) s p.Office =" "Moscow" "w p.Name," "" ", p.SSN ,! q 0")
In parentheses at the beginning lines indicate input parameters. All other variables will be searched in the current context. In the example below, in is a formal parameter, and the value of b is taken from the current context
USER> s a = "(in) ret in + b"
USER> s b = 10 w $ xecute (a, 2)
12
USER> sb = 13 w $ xecute (a, 2)
15
The disadvantage of using $ xecute to pass code is that you cannot put more than a few commands on the same line. In addition, syntax checking will be performed only during code execution. On the other hand, there is no need to produce methods that will be used only once.
However, there are ways to mitigate these limitations.
Under the cat, consider several options for passing code as an argument to a subroutine.
Suppose there are two of the following methods:
ClassMethod AllPersonsWithA ()
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare ("select ID from Sample.Person where substr (name, 1,1) = 'A '")
do rs.Execute ()
while rs.Next () {
set p = ## class (Sample.Person).% OpenId (rs.Get ("ID"))
set p.Office = "Moscow"
write p.Name, "", p.SSN !
kill p
}
kill rs
}
ClassMethod AllCompaniesWithO ()
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare ("select ID from Sample.Company where substr (name, 1,1) = 'O '")
do rs.Execute ()
while rs.Next () {
set p = ## class (Sample.Company).% OpenId (rs.Get (" ID "))
set p.Name =" OOO "_p. Name
write p.Name ,!
kill p
}
kill rs
}
To switch to the new version of dynamic SQL -% SQL.Statement we have to change the code in two places. If at us% ResultSet was used in ten places, would change in ten.
We rewrite these two methods as follows.
Add a method to handle a specific instance of Sample.Person.
ClassMethod ProcessPerson (p As Sample.Person)
{
set p.Office = "Moscow"
write p.Name, "", p.SSN ,!
}
With this command, we replace the AllPersonsWithA method:
do ..OpenAndProcess ("select ID from Sample.Person where substr (name, 1,1) = 'A'", "Sample.Person", "ProcessPerson")
Here, the first argument is the query, the second is the name of the class whose instances will be processed, the third is the name of the class method that needs to be called for each row of the query result.
For companies, the processing method will look like this:
ClassMethod ProcessCompany (c As Sample.Company)
{
set c.Name = "OOO" _c.Name
write c.Name ,!
}
We will
call the following command: do ..OpenAndProcess ("select ID from Sample.Company where substr (name, 1,1) = 'O'", "Sample.Company", "ProcessCompany")
Now, in fact, the OpenAndProcess method itself :
ClassMethod OpenAndProcess (query As% String, className As% String, callback As% String)
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare (query)
do rs.Execute ()
while rs.Next () {
set p = $ classmethod (className, "% OpenId", rs.Get ("ID"))
do $ classmethod ($ classname () , callback, p);
kill p
}
kill rs
}
The function $ classmethod (class, method, arg1, arg2, ...) calls the class method called method from the class class and passes arg1, arg2, etc. to it.
Now, working with% ResultSet has been moved to a separate method, what is being done there does not interest anyone.
Obviously, the OpenAndProcess method can be fixed so that it does not call the method from the current, but from an arbitrary class and passes an arbitrary number of parameters to callback.
If the code for callback is very small, you can use the $ xecute function, which is a kind of analogue of anonymous functions. The OpenAndProcess method in this case would look like:
ClassMethod OpenAndProcess (query As% String, className As% String, callback As% String)
{
set rs = ## class (% ResultSet).% New ()
do rs.Prepare (query)
do rs.Execute ()
while rs.Next () {
set p = $ classmethod (className, "% OpenId", rs.Get ("ID"))
set res = $ xecute (callback, p)
kill p
}
kill rs
}
And the processing would not be enclosed in a class method, but in a string.
set query = "select ID from Sample.Person where substr (name, 1,1) = 'A'"
do ..OpenAndProcess (query, "Sample.Person", "(p) s p.Office =" "Moscow" "w p.Name," "" ", p.SSN ,! q 0")
In parentheses at the beginning lines indicate input parameters. All other variables will be searched in the current context. In the example below, in is a formal parameter, and the value of b is taken from the current context
USER> s a = "(in) ret in + b"
USER> s b = 10 w $ xecute (a, 2)
12
USER> sb = 13 w $ xecute (a, 2)
15
The disadvantage of using $ xecute to pass code is that you cannot put more than a few commands on the same line. In addition, syntax checking will be performed only during code execution. On the other hand, there is no need to produce methods that will be used only once.