Written by

Senior Analyst at WoodWare Systems
Question David Hockenbroch · Oct 14, 2021

Indirection Different In Terminal Than In Method

If I open a terminal and type the following commands, sc is an error:

set validator = "sc = ##class(%Library.Numeric).IsValid(""BLAH"")"
set @validator
write sc

At the end, when I write sc I get:

0 L'BLAH9
             DOCXT010,#e^zIsValid+1^%Library.Numeric.1^1e^^^1

However, if I call the following class method using the arguments "%Library.Numeric" and "BLAH", sc is undefined

ClassMethod testvalidator(class As %String, value As %String) As %Status{set validator = "sc = ##class("_class_").IsValid("""_value_""")"write validator,!set @validatorwrite sc,!quit sc}

When it tries to write sc, I get:

write sc,!
 ^
<UNDEFINED>zvalidator+4^DXT4.tagvalue.1 *sc

And I can't figure out what's making that different. Why would this behave differently in this method than doing the same thing line by line in the terminal?

Product version: Caché 2018.1
$ZV: Cache for UNIX (Red Hat Enterprise Linux for x86-64) 2018.1.2 (Build 309U) Mon Mar 4 2019 15:07:46 EST

Comments

Julius Kavay · Oct 14, 2021

You have a problem with the scoping!

Indirection has a global scoping, you have to put things with indirection in a global scope:

ClassMethod testvalidator(class As %String, value As %String) As %Status [ PublicList = (validator, sc) ]
{
   new validator, sc
   set validator = "sc = ##class("_class_").IsValid("""_value_""")"
   write validator,!
   set @validator
   write sc,!
   quit sc
}
set result = ##class(...).testvalidator("%Library.Numeric","BLABLA")
do $system.OBJ.DisplayError(result) --> ERROR #7207: Datatype value 'BLABLA' is not a valid number
0
David Hockenbroch  Oct 14, 2021 to Julius Kavay

Thank you, Julius! It was that, and my class wasn't ProcedureBlock.

0
Sergei Shutov  Oct 14, 2021 to David Hockenbroch

All classes are ProcedureBlock by default, you need to manually set [Not ProcedureBlock] if you don't want to compile classes to Procedures. It's highly recommended to use ProcedureBlock though.

0
David Hockenbroch  Oct 15, 2021 to Sergei Shutov

I was updating code someone else wrote a few years ago, and they had set [Not ProcedureBlock] on the class. I'm not sure why.

0
Julius Kavay  Oct 15, 2021 to David Hockenbroch

As @Sergei Shutov pointed out, you can switch off the procdere block by a keyword for the whole class. Additionaly, you can switch on or off the procedure block keyword for a particular  method too. In your case:

class Some.Class  Extends %RegisteredObject
{
/// a procedure block method
ClassMethod ProcBlock()
{
}

/// a nonprocedurblock method
ClassMethod NoProcBlock() [ ProcedureBlock = 0 ]
{
// Caution: All variables have a global scope, hence, they will overwrite variables with the same name, which were created previously. To avoid this, use the NEW command, to protect them (if desired).
}

}
0
Vitaliy Serdtsev · Oct 15, 2021

Use $CLASSMETHOD and you won't have such issues:

USER><FONT COLOR="#0000ff">set </FONT><FONT COLOR="#800000">sc</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#0000ff">$CLASSMETHOD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#008000">"%Numeric"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"IsValid"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"BLAH"</FONT><FONT COLOR="#000000">)</FONT>

or

<FONT COLOR="#000080">ClassMethod </FONT><FONT COLOR="#000000">testvalidator(</FONT><FONT COLOR="#ff00ff">class </FONT><FONT COLOR="#000080">As %String</FONT><FONT COLOR="#000000">, </FONT><FONT COLOR="#ff00ff">value </FONT><FONT COLOR="#000080">As %String</FONT><FONT COLOR="#000000">) </FONT><FONT COLOR="#000080">As %Status </FONT><FONT COLOR="#000000">{  </FONT><FONT COLOR="#0000ff">set </FONT><FONT COLOR="#800000">sc</FONT><FONT COLOR="#000000">=</FONT><FONT COLOR="#0000ff">$CLASSMETHOD</FONT><FONT COLOR="#000000">(</FONT><FONT COLOR="#800000">class</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#008000">"IsValid"</FONT><FONT COLOR="#000000">,</FONT><FONT COLOR="#800000">value</FONT><FONT COLOR="#000000">)  </FONT><FONT COLOR="#0000ff">write </FONT><FONT COLOR="#800000">sc</FONT><FONT COLOR="#000000">,!  </FONT><FONT COLOR="#0000ff">quit </FONT><FONT COLOR="#800000">sc </FONT><FONT COLOR="#000000">}</FONT>

0
Julius Kavay  Oct 15, 2021 to Vitaliy Serdtsev

He (@David Hockenbroch) is playing with inderection, using $classmethod() instead of ##class(classname).methodname(...)  does not solve the scoping problem:

ClassMethodtestvalidator(classAs%String, value As%String) As%Status
{  
  setvalidator = "sc = $classmethod(class, ""IsValid"", value)"
   writevalidator,!
   set@validator
   writesc,!
   quitsc
}

The above method gives you the same <UNDEF> error because of non global scoping! By using indirection both variables (validator and sc) must have global scope.

0
Sergei Shutov  Oct 15, 2021 to Julius Kavay

Well in case of $classmethod you don't need to use an indirection at all to achieve the same result. I would avoid using indirection if at all possible.

0
Robert Cemper  Oct 15, 2021 to Sergei Shutov

@Sergei Shutov     You missed the point.
validator
is typically stored as part of the individual data record to handle various types of checks within your class to provide the highest flexibility. Think of language or geographically related checking.
You can't bypass the challenge.
Neither by $classmetho() nor any code generator as this is all static code frozen and inflexible a runtime. 

Creating a check routine by field or by record is not a realistic solution 

0
Eduard Lebedyuk  Oct 18, 2021 to Robert Cemper

You can't bypass the challenge.
Neither by $classmetho() nor any code generator as this is all static code frozen and inflexible a runtime.  

Why?

A case where user must be able to:

  • create a code snippet with a predetermined interface
  • choose it for execution among all the code snippets with the same interface

Can be solved in many ways, without indirection (aka executing code stored as a string).

I usually provide a base class, which a user must extend and populate with his own methods. After that in the source app, just call SubclassOf Query from %Dictionary.ClassDefinition to get all implementation and show them to a user to pick from.

Works good enough.

0
Julius Kavay  Oct 15, 2021 to Sergei Shutov

The question was (see the original post),  why does a command work in a terminal but not in a method. And, as you know, the answer lies in the scoping. I would say, he (@David Hockenbroch) tries to learn and understand the nature of indirection and is not working on a production grade problem.

But maybe I'm wrong... who knows.

0
Vitaliy Serdtsev · Oct 15, 2021
ClassMethod testvalidator(
  class As %String,
  value As %StringAs %Status
{
 set validator "(out){set out = ##class("_class_").IsValid("""_value_""")}"
 xecute (validator,.sc)

 write sc,!
 quit sc
}
0