How to make Mathematica variables declarative instead of just-in-time?

31

10

Is there a way to have Mathematica at the notebook level (SetOptions[EvaluationNotebook[], CellContext -> Notebook]) only uses a set of declared variables in the notebook context. Any variables that are not declared would throw a warning on use. A front-end indication (font format) and/or a run-time warning.

For example, Visual Basic as the OPTION EXPLICIT command which throws a warning if any variables are used that have not been explicitly declared. When this command is not set you can create variables on the fly as you can in Mathematica.

I am developing a UI for a process that as many user driven event updates. I have a quite a few variables that are holding state of this process and these are all floating about in the notebook context. It is easy to typo one of the variable names and then it is a nightmare to locate it.

Short of wrapping the entire code of the notebook in one giant module where misspelled names would show blue, how do I get some front-end indication or run-time warning that a variable is undeclared?


Update

Using @rcollyer answer below that pointed me to $NewSymbol I have constructed the following two functions, declareContext and removeDeclareContext, that enable an OPTION EXPLICIT type environment when the Global setting for CellContext is Notebook (this can be done in the Option Inspector).

ClearAll[declareContext];
declareContext[context_String] :=
 Module[{},
  $NewSymbol::undeclared = "`1``2` was not previously declared.";

  If[ValueQ[$NewSymbol::declarativeContexts] == False,
   $NewSymbol::declarativeContexts = ""
   ];

  $NewSymbol::declarativeContexts = 
   StringJoin[$NewSymbol::declarativeContexts, "|", context];

  $NewSymbol :=
   If[ContainsAny[StringSplit[$NewSymbol::declarativeContexts, "|"], {#2}] &&
      ContainsNone[Names[#2 <> "*"], {#1}],
     Message[$NewSymbol::undeclared, #2, #1]] &;
  ]

declareContext sets up

  • a new message to display for undeclared variables,
  • a repository of contexts that are participating in the option explicit setting by using a new message, $NewSymbol::declarativeContexts,
  • the $NewSymbol to a function that makes use of both its parameters (variable and context) to check the context repository and existing variables in the context.

ClearAll[removeDeclareContext];
removeDeclareContext[context_String] :=
 $NewSymbol::declarativeContexts = 
  StringDelete[$NewSymbol::declarativeContexts, "|" <> context]

removeDeclareContext simply removes the context from the repository.

Both of the above can be call on multiple notebooks and notebooks that have not called them will continue to operate with just-in-time variables. These can be placed in a .wl package file to make them easier to use across multiple notebooks.

To use

At the start of your notebook declare your variables by evaluating them in a list. Then call declareContext with the Context; this should be the notebook context with either the notebook or global preference CellContext -> Notebook.

{x, y, z};
declareContext@Context[]

You may now assign values to the declared variables but new variables will error.

In:= x = 4
Out= 4

but

w = 1
$NewSymbol::undeclared: Notebook$$15$56114`w was not previously declared. >>

Turn off declarative variables by calling removeDeclareContext@Context[].

Issues

The functions above prevent declaring scoped variables as in Module and DynamicModule. Therefore they are too restrictive.

Module[{w},
 w = 1
]
$NewSymbol::undeclared: Notebook$$15$56114`w was not previously declared. >>

You can work around this by using a sub-context for scoped variables.

Module[{m`w},
 m`w = 1
]
1

Any ideas to loosen up the functions so that you do not need a sub-context for scoped variables are very welcome

Edmund

Posted 2017-02-02T18:51:35.987

Reputation: 35 657

For the context repository, I'd give you another +1, if I could. Clever workaround. – rcollyer – 2017-02-02T21:11:33.167

1For debugging I'd use a palette with dynamically updated table of symbols which have values. – Kuba – 2017-02-02T21:44:38.737

Answers

14

Edit: method extended for multiple contexts and unlocking mehtod added.


Let's protect whatever is a new symbol.

In old answer I've manually excluded symbols matching name$digits but that wasn't necessary as according to $NewSymbol details:

$NewSymbol is not applied to symbols automatically created by scoping constructs such as Module.

enter image description here

 BeginPackage["Lock`"];

    contextLock; contextUnlock;

 Begin["`Private`"];

   SetAttributes[{contextLock, contextUnlock}, HoldFirst];

   $LockedContexts = <||>;

   contextLock[context_: $Context] := Which[
     $LockedContexts === <||>
     , setLocking[]; $LockedContexts[context] = {}
     , Not@KeyExistsQ[$LockedContexts, context]
     , $LockedContexts[context] = {};
   ];



   contextUnlock[context_: $Context] /; KeyExistsQ[$LockedContexts, context] := (
       ToExpression[#, StandardForm, Unprotect] & /@ $LockedContexts[context]
     ; ToExpression[#, StandardForm, Remove] & /@ $LockedContexts[context]
     ; KeyDropFrom[$LockedContexts, context]
     ; If[$LockedContexts === <||>, $NewSymbol =.];
   )

   setLocking[] := $NewSymbol := If[
     MemberQ[Keys[$LockedContexts], #2]
     , AppendTo[$LockedContexts[#2], #2 <> #1]
     ; ToExpression[#2 <> #1, StandardForm, Protect]
   ] &;

End[];

EndPackage[];

Kuba

Posted 2017-02-02T18:51:35.987

Reputation: 129 207

Okay. The big drawback of your answer is that only once notebook context can be locked at a time. If I call Lock`contextLock[] on notebook A it removes the lock on notebook B. Will have to update my question update to use the repository. – Edmund – 2017-02-03T14:02:34.393

1@Kuba to get around the single context, you could use an Association as a set as the keys are guaranteed to be unique, so you can use MemberQ[Keys@assoc, #2] to test if it is a protected context. – rcollyer – 2017-02-04T02:43:10.907

@Edmund updated – Kuba – 2017-02-05T19:47:18.213

@rcollyer updated, don't have time to add examples but will do that soon. it is quite selfexplanatory though. – Kuba – 2017-02-05T19:48:00.627

Adding Protect was a good method of preventing Set instead of just warning the user that it was occurring. Unfortunately, the symbol is still added to the context, so if you unlock and then re-lock all those previously protected symbols are now available. I suppose they could be Removed, instead of just Unprotecting them. Although, that could be made user settable. Also, contextLock can be simplified to – rcollyer – 2017-02-05T21:27:21.047

(cont'd) to contextLock[context_: $Context] := ( If[$LockedContexts === <||>, setLocking[]]; $LockedContexts[context] = Lookup[$LockedContexts, context, {}]~Join~{}; context );. Similarly, I'd swap out the Append in setLocking to $LockedContexts[#2] = $LockedContexts[#2]~Join~{#2 <> #1};, but I think that is more aesthetics, than any actual effect. Lastly, I'd have contextUnlock also return context, like I had contextLock return. Better feedback to the user. – rcollyer – 2017-02-05T21:29:52.753

@rcollyer I decided that I don't care and OP probably isn't concered about those symbols being created and the main goal is to find OP's typos in symbols' names :) Yep, AppendTo is not a good practice but here is it more readable and won't harm. And yep, a lot of things can be added, like preventing $LockedContexts from being manually modified but I don't want to overcommit if I don't know if that fits OP needs. Tips on point of course, thanks. – Kuba – 2017-02-06T07:20:16.930

@kuba not caring is definitely a good stopping point. :) – rcollyer – 2017-02-06T13:11:05.843

@Edmund you are requested to clean old comments and give a feedback about the update :) – Kuba – 2017-02-08T07:35:31.503

I think on unlocking the context I would Remove all Protected symbols in the context that had Definition[_Symbol] === Null. I believe that would restrict the removal to only those symbols protected by the context lock. – Edmund – 2017-02-08T13:47:32.733

@Edmund I would not, if Remove then only those that were locked by this function. Why complicate things? So you can add a Remove procedure just after Unprotecting if you need. – Kuba – 2017-02-08T13:51:07.640

1My thoughts are that I would unlock the context to explicitly declare additional variables. If there are unused variable names that are locked then this excludes them from being used. It also seems cleaner not to have the failed symbols lurking around the context once it is unlocked as all symbols should be available when in the unlocked state. – Edmund – 2017-02-08T13:56:42.040

@Edmund That is a very good point! – Kuba – 2017-02-08T13:58:35.623

26

You are looking for $NewSymbol which is run every time a new symbol is created. For example, let say you only want x, y, and z as symbols, then declare them initially

In[63]:= {x, y, z}
(*Out[1]= {x, y, z}*)

Then, set $NewSymbol to issue a message when it is used, e.g.

In[2]:= $NewSymbol::undeclared = "`1` was not previously declared.";
In[3]:= $NewSymbol := Message[$NewSymbol::undeclared , #1] &

In[4]:= q
(*
During evaluation of In[4]:= $NewSymbol::undeclared: q was not previously declared.
Out[4]= q*)

But, no message is issued with x.

In[5]:= x = 5
(*Out[5]= 5*)

Additionally, you can create your own cell style that will treat variable declarations as expected using a custom CellEvaluationFunction

CellEvaluationFunction -> (Block[{$NewSymbol}, ToExpression[#]]&)

For instance, you can add it to the "Code" cell, e.g.

Cell[StyleData["Code"], 
 CellEvaluationFunction -> (Block[{$NewSymbol}, ToExpression[#]]&)
]

rcollyer

Posted 2017-02-02T18:51:35.987

Reputation: 32 561

3Very good answer. (+1) Going to let this sit for a bit just in case. – Edmund – 2017-02-02T19:09:20.100

@Edmund never a problem. I like seeing what others come up with, too. – rcollyer – 2017-02-02T19:10:44.110

Does $NewSymbol affect all context or just the context it is set in? Since I use the Notebook context I can't have $NewSymbol from notebook A affect $NewSymbol from notebook B. – Edmund – 2017-02-02T19:11:24.620

1Good point. It takes two arguments, the second being the context. A filter can be added to that to make it more robust ... how to make it understand what notebook B's context is automatically would take some digging, but if declared in the notebook, a simple comparison with Context[] should be sufficient. – rcollyer – 2017-02-02T19:14:36.797

@Edmund thanks for the gold badge. :) – rcollyer – 2017-02-09T13:57:45.883