X
13241 Rate this article:
No rating

Dynamic Keyword Validation using ROUTINE_INFO

Anonym

  In IDL, there are three variants of the _EXTRA keyword: _EXTRA, _REF_EXTRA, and _STRICT_EXTRA.  The _EXTRA and _REF_EXTRA keywords are used to define the signature of the routine, while you can use either _EXTRA or _STRICT_EXTRA when you invoke any routine.  As my colleague Jim Pendleton wrote about a few years ago, the _REF_EXTRA keyword is a potential improvement over plain old _EXTRA.  This is because it uses pass by reference semantics instead of the pass by value semantics that _EXTRA does, so you don’t make copies of the variables at each function call.  There is the downside that the values passed by _REF_EXTRA are mutable, so some care needs to be taken there, but it can save you a lot of memory and time allocating those copies.

  The downside of both _EXTRA and _REF_EXTRA is that they are generic grab bags that allow the caller to call your function with any superfluous keywords they want, with no way of knowing which keywords are actually used and which are extraneous.  Enter the _STRICT_EXTRA keyword, which is discussed in the help documentation of “Keyword Inheritance”.  Normally when you call a routine, you use _EXTRA keyword as the way to pass in the collection of keywords to the routine.  Any keywords in the _EXTRA bag that are part of the routine signature will be properly mapped, and whatever is left will be captured by _EXTRA/_REF_EXTRA if it’s present, or dropped on the floor otherwise.  If you were to call the same routine using _STRICT_EXTRA instead of _EXTRA, then it will throw an error if there are any keywords that do not map to the routine signature.  We can see this in action in this contrived example, which uses minimal procedures to illustrate the validation logic:

pro callByValue, _EXTRA=extra
  help, extra
end
pro callByReference, _REF_EXTRA=refExtra
  help, refExtra
end
pro callWithNoExtra, FOO=foo
  help, foo
end
pro callWrapper, _REF_EXTRA=extra
  callByValue, _EXTRA=extra
  callByReference, _EXTRA=extra
  callWithNoExtra, _EXTRA=extra
end
pro callStrictWrapper, _REF_EXTRA=extra
  callByValue, _STRICT_EXTRA=extra
  callByReference, _STRICT_EXTRA=extra
  callWithNoExtra, _STRICT_EXTRA=extra
end
pro extra_tests, _REF_EXTRA=extra
  callWrapper, PI=!pi
  callStrictWrapper, PI=!pi
end

 

The output from running extra_tests is:

** Structure <1307e5a0>, 1 tags, length=4, data length=4, refs=1:
   PI              FLOAT           3.14159
REFEXTRA        STRING    = Array[1]
FOO             UNDEFINED = <Undefined>
** Structure <1307e650>, 1 tags, length=4, data length=4, refs=1:
   PI              FLOAT           3.14159
REFEXTRA        STRING    = Array[1]
% Keyword PI not allowed in call to: CALLWITHNOEXTRA
% Execution halted at: CALLSTRICTWRAPPER   32 C:\Users\brian\IDLWorkspace83\Default\extra_tests.pro
%                      EXTRA_TESTS        43 C:\Users\brian\IDLWorkspace83\Default\extra_tests.pro
%                      $MAIN$

  The _EXTRA and _REF_EXTRA keywords are able to accept the PI keyword without incident.  When callWrapper invokes callWithNoExtra, the PI keyword is dropped on the floor, and FOO is left undefined.  But when callStrictWrapper invokes callWithNoExtra, the PI keyword does not line up with the routine signature and an error is thrown.  It is important to note that callByValue and callByReference work perfectly fine with _STRICT_EXTRA, as their _EXTRA and _REF_EXTRA keywords respectively will swallow up the PI keyword no problem.

  So _STRICT_EXTRA has its limitations, particularly in the case where you want to call more than one routine that doesn’t include a form of _EXTRA.  To accomplish this we need to query the IDL runtime to get information about the routine signatures using the powerful ROUTINE_INFO function.  When you use this function with its /PARAMETERS keyword, it will return a structure that lets you get the list of all the keywords in a given routine’s signature.  Let’s look at a simple example to see what ROUTINE_INFO returns:

pro myPro, PARAM1=p1, PARAM2=p2, PARAM3=p3
  print, 'in myPro'
  help, p1, p2, p3
end

function myFunc, PARAM2=p2, PARAM4=p4
  print, 'in myFunc'
  help, p2, p4
  return, 0
end

IDL> info1 = Routine_Info('myPro', /PARAMETERS)
IDL> info2 = Routine_Info('myFunc', /PARAMETERS, /FUNCTIONS)
IDL> info1
{
    NUM_ARGS: 0,
    NUM_KW_ARGS: 3,
    KW_ARGS: ["PARAM1" "PARAM2" "PARAM3"]
}
IDL> info2
{
    NUM_ARGS: 0,
    NUM_KW_ARGS: 2,
    KW_ARGS: ["PARAM2" "PARAM4"]
}

  The implied print output for a struct is convenient as it gives you the tags, but also expands arrays, which neither help nor print will do for a struct.  You’ll notice that I had to add the /FUNCTIONS keyword when I was requesting info about a function instead of a procedure.  So I can use the KW_ARGS member of the info struct to identify which members of the _REF_EXTRA bag to use for each routine invocation.  I can also use it to verify that there aren’t any superfluous keywords passed into the wrapper.

  Once you’ve verified that the keywords passed into the wrapper are valid, you then need to construct the subset of values that are to be passed into each wrapped routine.  The way to do this dynamically is to manually construct a struct to pass into the _EXTRA keyword.  We do this incrementally by adding only those keywords and values to a struct using CreateStruct to append new key/value pairs.  When using _REF_EXTRA, you get a string array that includes the keywords used to invoke your method.  To get the values for each keyword you have to use Scope_VarFetch with its /REF_EXTRA keyword to get a reference to that value in the local scope.  Here is an example wrapper for the myPro and myFunc routines defined above:

function myDynamicReferenceWrapper, _REF_EXTRA=refExtra
  compile_opt idl2
  ; first make sure _REF_EXTRA is defined, bail if not
  if (~ISA(refExtra)) then return, -1

  info1 = Routine_Info('myPro', /PARAMETERS)
  info2 = Routine_Info('myFunc', /PARAMETERS, /FUNCTION)

  ; check for invalid keywords in _REF_EXTRA
  foreach keyword, refExtra do begin
    if ((Total(keyword eq info1.KW_ARGS) eq 0) && $
        (Total(keyword eq info2.KW_ARGS) eq 0)) then begin
      Message, 'Invalid keyword ' + keyword
    endif
  endforeach

  ; call myPro with the appropriate keywords from _REF_EXTRA
  extra1 = {}
  foreach keyword, info1.KW_ARGS do begin
    w = where(keyword eq refExtra, found)
    if (found gt 0) then begin
      value = Scope_VarFetch(keyword, /REF_EXTRA)
      extra1 = Create_Struct(extra1, keyword, value)
    endif
  endforeach
  myPro, _EXTRA=extra1

  ; call myFunc with the appropriate keywords from _REF_EXTRA
  extra2 = {}
  foreach keyword, info2.KW_ARGS do begin
    w = where(keyword eq refExtra, found)
    if (found gt 0) then begin
      value = Scope_VarFetch(keyword, /REF_EXTRA)
      extra2 = Create_Struct(extra2, keyword, value)
    endif
  endforeach
  return, myFunc(_EXTRA=extra2)
end

  If you had concerns about the wrapped methods modifying your variables and want to use _EXTRA instead of _REF_EXTRA, then there are a couple tweaks to this wrapper:

function myDynamicValueWrapper, _EXTRA=extra
  compile_opt idl2
  ; first make sure _EXTRA is defined, bail if not
  if (~ISA(extra)) then return, -1

  info1 = Routine_Info('myPro', /PARAMETERS)
  info2 = Routine_Info('myFunc', /PARAMETERS, /FUNCTIONS)

  ; check for invalid keywords in _EXTRA
  myExtraKeywords = Tag_Names(extra)
  foreach keyword, myExtraKeywords do begin
    if ((Total(keyword eq info1.KW_ARGS) eq 0) && $
        (Total(keyword eq info2.KW_ARGS) eq 0)) then begin
      Message, 'Invalid keyword ' + keyword
    endif
  endforeach

  ; call myPro with the appropriate keywords from _EXTRA
  extra1 = {}
  foreach keyword, info1.KW_ARGS do begin
    w = where(keyword eq myExtraKeywords, found)
    if (found gt 0) then begin
      extra1 = Create_Struct(extra1, keyword, extra.(w[0]))
    endif
  endforeach
  myPro, _EXTRA=extra1

  ; call myFunc with the appropriate keywords from _EXTRA
  extra2 = {}
  foreach keyword, info2.KW_ARGS do begin
    w = where(keyword eq myExtraKeywords, found)
    if (found gt 0) then begin
      extra2 = Create_Struct(extra2, keyword, extra.(w[0]))
    endif
  endforeach
  return, myFunc(_EXTRA=extra2)
end

  Either of these wrappers could be made truly generic by adding string array parameter that was the set of routine names you want wrapped, instead of having myPro and myFunc hardcoded.