Written by

Technical Trainer and Course Developer at InterSystems
Question Joel Solon · 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:

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

Eduard Lebedyuk · Feb 18, 2016

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

0