Variable scope in .mac routine?

Given the following code:

 

start
    set tstVar = "Green"
    do TestIt()
 
TestIt() {
    write tstVar
}

 

I get <UNDEFINED>start+4^DeploymentTool *tstVar
 

I also tried setting the variable outside the start method but that doesn't work either.  I realize I could pass tstVar to the TestIt() routine but I'm trying to create a couple variables that will be reused repeatedly so that doesn't really work.

  • 0
  • 0
  • 285
  • 11
  • 0

Comments

Got it!

Variable scoping determines when a variable is “visible” to a program. In Caché ObjectScript, there are two sets of rules for variable scoping:
 -  The standard (and newer) scoping mechanism, which is based on procedure blocks. This is the preferred mechanism for new applications and is the default used by the Caché Studio. Within a procedure block, all non-% variables are private variables.


 set %tstVar = "Green"

Please note, that another approach (put tstVar in the "public" list) suggested by Timothy Leavitt  works better in most cases, as once defined a % variable can be modified anywhere within the process. With "public" list you retain full control over variable visibility which makes debugging much easier.

Here's some relevant documentation: http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GORIENT_ch_cos#GORIENT_cos_scope

One way to make this work as-is would be to put tstVar in the "public" list:

start
    set tstVar = "Green"
    do TestIt()
 
TestIt() [tstVar] {
    write tstVar 
}

Another would be to give the variable a name starting with a %:

start
    set %tstVar = "Green"
    do TestIt()
 
TestIt() {
    write %tstVar 
}

Taking into account that Scott wanted to pass multiple variables between routines, then %-named variables could serve better, making write less code. But...

To prevent variable leak outside of top-most start routine you better to New all %-named variables, e.g.

start

    new %tstVar
    set %tstVar  =  "Green"
    do TestIt()

    quit
 
TestIt() {
    write %tstVar 
}

Internally new statement creates new frame in the variables symbol table (which means virtually it's the new symbol table content), and this %-named entry will be accessible in the inner routines, even if they are of "new procedure block kind" like TestIt function here.

But, importantly, when control flow will return back to the outer start frame and quit statement will be called, it will not only return control to the proper place in caller, but it will also cleanup all new-ed variables, thus effeciantly returning back previous state of symbol table. [I do not mean it will return back variables values, I mean return back presence or absence of some variable name]

P.S.

If you know Perl then it's like local "function" work for localization scope of variables.

In my personal opinion, this is already a case where you want to switch from a routine to a class design. This way it is so much easier to keep track of the scope of your variables by defining them as properties of your class.

Working with publiclist and %-variables in routines can introduce a couple of side-effects, especially if these are not documented properly.

Yes, agreed with Stefan - if you need to keep track of more than a couple of variables in context between function calls then this is indication that you need to create object with multiple properties in its context.

Thanks for the input.

Ignoring what I posted above, if you were going to write an implementation script that took input and modified some registry values, what would you use?

In various posts, you've talked about having an implementation tool that is run from the browser, and you've also talked about running it from terminal. Here's a design that would be well-suited to both:

* Settings that apply to the whole thing are properties of an object (class extends %RegisteredObject)

* There's a class method (static method in other languages) to get the settings' values interactively

* There's an instance method to run the setup on an instance of the class

Include %syPrompt

Class Demo.SetupTool Extends %RegisteredObject
{

Property Type As %String(VALUELIST = ",Gateway,IHE");

Property Setting1Value As %String [ InitialExpression = "ABC" ];

Property Setting2Value As %Boolean [ InitialExpression = ];

ClassMethod RunInteractive()
{
    Set tSettingsObject = ..%New()
    
    Write "Deployment Utility"
    Set options(1) = "Gateway"
    Set options(2) = "IHE"
    Do ##class(%Prompt).GetArray("Deployment Type?",.tType,.options,,,,$$$InitialDisplayMask+$$$MatchArrayMask)
    Set tSettingsObject.Type = tType
    
    Set tString = tSettingsObject.Setting1Value
    Do ##class(%Prompt).GetString("Enter a string: ",.tString)
    Set tSettingsObject.Setting1Value = tString
    
    Set tYesOrNo = tSettingsObject.Setting2Value
    Do ##class(%Prompt).GetYesNo("Is this a helpful demo? ",.tYesOrNo)
    Set tSettingsObject.Setting2Value = tYesOrNo
    
    Do tSettingsObject.DoSetup()
}

Method DoSetup()
{
    Write !,"Starting ",..Type," deployment...",!
    // Here, do things based on properties of this object.
    If '..Setting2Value {
        //Do something specific if Setting2Value is false.
        Write "Why not?",!
    }
}

}
 

Then, for example:

USER>d ##class(Demo.SetupTool).RunInteractive()
Deployment Utility
 
1) Gateway
2) IHE
 
Deployment Type? 1 Gateway
Enter a string:  ABC =>
Is this a helpful demo?  Yes => No
Starting Gateway deployment...
Why not?

 

Or, if you want to do the same kind of setup from a simple ZenMethod in a Zen page or something, you could set up a settings object and then call DoSetup() on it.

USER>set tSettings = ##class(Demo.SetupTool).%New()
 
USER>set tSettings.Type = "IHE"
 
USER>d tSettings.DoSetup()
 
Starting IHE deployment...