Question
David Hockenbroch · Oct 14

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 @validator
write 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
00
2 0 12 231
Log in or sign up to continue

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

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

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.

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.

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

}

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


USER>set sc=$CLASSMETHOD("%Numeric","IsValid","BLAH")

or

ClassMethod testvalidator(class As %Stringvalue As %StringAs %Status
{
 set sc=$CLASSMETHOD(class,"IsValid",value)
 write sc,!
 quit sc
}

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

ClassMethod testvalidator(class As %String, value As %String) As %Status
{  
  set validator = "sc = $classmethod(class, ""IsValid"", value)"
   write validator,!
   set @validator
   write sc,!
   quit sc
}

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.

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.

@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 

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.

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.

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
}