Variable Number of Arguments to a Method
This morning on the old Caché Google Group, someone posed the following question, which I've decided to answer here, because it's interesting!
Is there a way to iterate ClassMethod's params, and get param's names and values?
The first answer I can come up with is: it's not easy! In any method, you could try to write code like this (where methodName is the name of your method):
set method = ##class(%Dictionary.MethodDefinition).IDKEYOpen($classname(), methodName)
set args = method.FormalSpec
for i=1:1:$length(args, ",") {
set arg = $piece($piece(args, ",", i), ":", 1)
write !, arg , " = ", @arg
}But the problem is that the @arg won't work, because indirection doesn't have access to the private variables of the method, so you'll get an <UNDEFINED>. You could decide to make the method not use ProcedureBlock ([ ProcedureBlock = 0 ]), or list all the arguments of the method in the PublicList of the method, so that the arguments are all public so that @arg works, but those seem like bad ideas to me.
So is the answer just: No? Well, not exactly. Another way is to use the "Variable Number of Arguments to a Method" technique, documented here:
http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls…
As you'll see from the docs, the method signature has to use three dots, like this:
Method Test(args... as %String)It's still not a generic solution; it forces you to write the method in a certain way ahead of time, if you want to be able to iterate through the arguments. But it does solve the problem in a straightforward way.
Anybody have any other ideas? Fire away!
Comments
I had a similar problem. The task was to write custom logging system, which would automatically store current method argument values. Here's how I done it.
First the the persistent log class (relevant parts):
Class App.Log Extends %Persistent
{
/// Replacement for missing values
Parameter Null = "Null";
/// Type of event
Property EventType As %String(MAXLEN = 10, VALUELIST = ",NONE,FATAL,ERROR,WARN,INFO,STAT,DEBUG,RAW") [ InitialExpression = "INFO" ];
/// Name of class, where event happened
Property ClassName As %String(MAXLEN = 256);
/// Name of method, where event happened
Property MethodName As %String(MAXLEN = 128);
/// Line of int code
Property Source As %String(MAXLEN = 2000);
/// Cache user
Property UserName As %String(MAXLEN = 128) [ InitialExpression = {$Username} ];
/// Arguments' values passed to method
Property Arguments As %String(MAXLEN = 32000, TRUNCATE = 1);
/// Date and time
Property TimeStamp As %TimeStamp [ InitialExpression = {$zdt($h, 3, 1)} ];
/// User message
Property Message As %String(MAXLEN = 32000, TRUNCATE = 1);
/// User IP address
Property ClientIPAddress As %String(MAXLEN = 32) [ InitialExpression = {..GetClientAddress()} ];
/// Add new log event
/// Use via $$$LogEventTYPE().
ClassMethod AddRecord(ClassName As %String = "", MethodName As %String = "", Source As %String = "", EventType As %String = "", Arguments As %String = "", Message As %String = "")
{
Set record = ..%New()
Set record.Arguments = Arguments
Set record.ClassName = ClassName
Set record.EventType = EventType
Set record.Message = Message
Set record.MethodName = MethodName
Set record.Source = Source
Do record.%Save()
}
}
And here's macros for client code:
#define StackPlace $st($st(-1),"PLACE")
#define CurrentClass ##Expression($$$quote(%classname))
#define CurrentMethod ##Expression($$$quote(%methodname))
#define MethodArguments ##Expression(##class(App.Log).GetMethodArguments(%classname,%methodname))
#define LogEvent(%type, %message) Do ##class(App.Log).AddRecord($$$CurrentClass,$$$CurrentMethod,$$$StackPlace,%type,$$$MethodArguments,%message)
#define LogNone(%message) $$$LogEvent("NONE", %message)
#define LogError(%message) $$$LogEvent("ERROR", %message)
#define LogFatal(%message) $$$LogEvent("FATAL", %message)
#define LogWarn(%message) $$$LogEvent("WARN", %message)
#define LogInfo(%message) $$$LogEvent("INFO", %message)
#define LogStat(%message) $$$LogEvent("STAT", %message)
#define LogDebug(%message) $$$LogEvent("DEBUG", %message)
#define LogRaw(%message) $$$LogEvent("RAW", %message)Now, how that works in client code? Let's say there is a class:
Include App.LogMacro
Class App.Use [ CompileAfter = App.Log ]
{
/// Do ##class(App.Use).Test()
ClassMethod Test(a As %Integer = 1, ByRef b = 2)
{
$$$LogWarn("Message")
}
}
In the int code, the $$$LogWarn macro would be transformed into:
Do ##class(App.Log).AddRecord("App.Use","Test",$st($st(-1),"PLACE"),"WARN","a="_$g(a,"Null")_"; b="_$g(b,"Null")_";", "Message")And after execution a new record would be added to App.Log table (note, that the method was called with default params - if it was called with other values they would be saved, as this logging system gets arguments values at runtime):
There is also some additional functionality, such as objects serializationinto json and context restoration at a later date, but that does not pertrain to the current discussion.
Anyway, the main idea is that at compile time we have a macro that:
-
Gets method arguments list from %Dictionary.CompiledMethod
-
For each argument decides on a strategy on how to get it's value at runtime
-
Writes source code that would implement value get at runtime
-
Builds code to get all method arguments values
-
Inserts this code into method
Relevant methods (in App.Log):
/// Entry point to get method arguments string
ClassMethod GetMethodArguments(ClassName As %String, MethodName As %String) As %String
{
Set list = ..GetMethodArgumentsList(ClassName,MethodName)
Set string = ..ArgumentsListToString(list)
Return string
}
/// Get a list of method arguments
ClassMethod GetMethodArgumentsList(ClassName As %String, MethodName As %String) As %List
{
Set result = ""
Set def = ##class(%Dictionary.CompiledMethod).%OpenId(ClassName _ "||" _ MethodName)
If ($IsObject(def)) {
Set result = def.FormalSpecParsed
}
Return result
}
/// Convert list of method arguments to string
ClassMethod ArgumentsListToString(List As %List) As %String
{
Set result = ""
For i=1:1:$ll(List) {
Set result = result _ $$$quote($s(i>1=0:"",1:"; ") _ $lg($lg(List,i))_"=")
_ ..GetArgumentValue($lg($lg(List,i)),$lg($lg(List,i),2))
_$S(i=$ll(List)=0:"",1:$$$quote(";"))
}
Return result
}
ClassMethod GetArgumentValue(Name As %String, ClassName As %Dictionary.CacheClassname) As %String
{
If $ClassMethod(ClassName, "%Extends", "%RegisteredObject") {
// it's an object
Return "_##class(App.Log).SerializeObject("_Name _ ")_"
} Else {
// it's a datatype
Return "_$g(" _ Name _ ","_$$$quote(..#Null)_")_"
}
}
The project is open-sourced and availible on GitHub (to use import all classes from App package into any namespace).