· Feb 18, 2016

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:

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!

Discussion (1)1
Log in or sign up to continue

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 = &quot;Null&quot;;

/// Type of event
Property EventType As %String(MAXLEN = 10, VALUELIST = &quot;,NONE,FATAL,ERROR,WARN,INFO,STAT,DEBUG,RAW&quot;) [ InitialExpression = &quot;INFO&quot; ];

/// 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&#39; 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 = &quot;&quot;, MethodName As %String = &quot;&quot;, Source As %String = &quot;&quot;, EventType As %String = &quot;&quot;, Arguments As %String = &quot;&quot;, Message As %String = &quot;&quot;)
    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),&quot;PLACE&quot;)
#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(&quot;NONE&quot;, %message)
#define LogError(%message)         $$$LogEvent(&quot;ERROR&quot;, %message)
#define LogFatal(%message)         $$$LogEvent(&quot;FATAL&quot;, %message)
#define LogWarn(%message)         $$$LogEvent(&quot;WARN&quot;, %message)
#define LogInfo(%message)         $$$LogEvent(&quot;INFO&quot;, %message)
#define LogStat(%message)         $$$LogEvent(&quot;STAT&quot;, %message)
#define LogDebug(%message)         $$$LogEvent(&quot;DEBUG&quot;, %message)
#define LogRaw(%message)         $$$LogEvent(&quot;RAW&quot;, %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)

In the int code, the $$$LogWarn macro would be transformed into:

Do ##class(App.Log).AddRecord(&quot;App.Use&quot;,&quot;Test&quot;,$st($st(-1),&quot;PLACE&quot;),&quot;WARN&quot;,&quot;a=&quot;_$g(a,&quot;Null&quot;)_&quot;; b=&quot;_$g(b,&quot;Null&quot;)_&quot;;&quot;, &quot;Message&quot;)

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 = &quot;&quot;
    Set def = ##class(%Dictionary.CompiledMethod).%OpenId(ClassName _ &quot;||&quot; _ 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 = &quot;&quot;
    For i=1:1:$ll(List) {
        Set result = result _ $$$quote($s(i&gt;1=0:&quot;&quot;,1:&quot;; &quot;) _ $lg($lg(List,i))_&quot;=&quot;)
        _ ..GetArgumentValue($lg($lg(List,i)),$lg($lg(List,i),2))
    Return result

ClassMethod GetArgumentValue(Name As %String, ClassName As %Dictionary.CacheClassname) As %String
    If $ClassMethod(ClassName, &quot;%Extends&quot;, &quot;%RegisteredObject&quot;) {
        // it&#39;s an object
        Return &quot;_##class(App.Log).SerializeObject(&quot;_Name _ &quot;)_&quot;
    } Else {
        // it&#39;s a datatype
        Return &quot;_$g(&quot; _ Name _ &quot;,&quot;_$$$quote(..#Null)_&quot;)_&quot;

The project is open-sourced and availible on GitHub (to use import all classes from App package into any namespace).