Occasionally it is necessary to temporarily assign a global flag to perform an action. Consider, for illustration, a procedure that returns the inert form of a procedure. We want it to be able to work with procedures that are local to modules. To do that, we need to temporarily assign the kernel flag opaquemodules to false.
GetInert1 := proc(p::uneval)
local opacity,inert;
opacity := kernelopts('opaquemodules'=false);
inert := ToInert(eval(p));
kernelopts('opaquemodules'=opacity);
inert;
end proc:
Here is a small module to test this on:
Test := module()
local f;
f := proc(x) x^2 end proc;
end module:
A quick test indicates that it worked:
GetInert1(Test[f]);
_Inert_PROC(_Inert_PARAMSEQ(_Inert_NAME("x")), _Inert_LOCALSEQ(),
_Inert_OPTIONSEQ(), _Inert_EXPSEQ(),
_Inert_STATSEQ(_Inert_POWER(_Inert_PARAM(1), _Inert_INTPOS(2))),
_Inert_DESCRIPTIONSEQ(), _Inert_GLOBALSEQ(), _Inert_LEXICALSEQ(),
_Inert_EOP(_Inert_EXPSEQ()))
However, suppose an error occurs during the evaluation of ToInert. For example,
GetInert1(Test[g]); Error, (in GetInert1) module does not export `g`
Because the error causes the procedure to terminate, the opaquemodules flag was never reset.
kernelopts(opaquemodules);
false
To avoid that bug, we use the try statement with a finally clause.
GetInert2 := proc(p::uneval)
local opacity,inert;
try
opacity := kernelopts('opaquemodules=false');
inert := ToInert(eval(p));
finally
kernelopts('opaquemodules' = opacity);
end try;
inert;
end proc:
Testing shows that this does, indeed, reset the flag when an error occurs.
kernelopts(opaquemodules=true): # manual reset from previous
GetInert2(Test[f]);
_Inert_PROC(_Inert_PARAMSEQ(_Inert_NAME("x")), _Inert_LOCALSEQ(),
_Inert_OPTIONSEQ(), _Inert_EXPSEQ(),
_Inert_STATSEQ(_Inert_POWER(_Inert_PARAM(1), _Inert_INTPOS(2))),
_Inert_DESCRIPTIONSEQ(), _Inert_GLOBALSEQ(), _Inert_LEXICALSEQ(),
_Inert_EOP(_Inert_EXPSEQ()))
GetInert2(Test[g]);
Error, (in GetInert2) module does not export `g`
kernelopts(opaquemodules);
true
The previous method works fine, however, it can be slightly improved. Rather than assigning the output of ToInert to a local variable (inert) and then returning that variable after the try statement, we can instead, directly return the output of ToInert. This must be done with an explicit return statement, otherwise the output of the kernelopts statement in the finally clause becomes the return value of the procedure.
GetInert3 := proc(p::uneval)
local opacity;
try
opacity := kernelopts('opaquemodules=false');
return ToInert(eval(p));
finally
kernelopts('opaquemodules' = opacity);
end try;
end proc:
Testing shows that it works properly:
GetInert3(Test[f]);
_Inert_PROC(_Inert_PARAMSEQ(_Inert_NAME("x")), _Inert_LOCALSEQ(),
_Inert_OPTIONSEQ(), _Inert_EXPSEQ(),
_Inert_STATSEQ(_Inert_POWER(_Inert_PARAM(1), _Inert_INTPOS(2))),
_Inert_DESCRIPTIONSEQ(), _Inert_GLOBALSEQ(), _Inert_LEXICALSEQ(),
_Inert_EOP(_Inert_EXPSEQ()))
GetInert3(Test[g]);
Error, (in GetInert3) module does not export `g`
kernelopts(opaquemodules);
true
Now that the code is working to satisfaction, we might consider whether it is useful to split it into reusable parts. Converting to an inert form is a rather specialized operation, something probably not done frequently. Extracting a local procedure from a module is more generally useful. So we'll make it a separate function, and then call it from GetInert.
Here is the procedure for extracting, if necessary, a procedure. A check has been added to verify that p does indeed evaluate to a procedure.
GetProc := proc(p::uneval)
local opacity;
try
opacity := kernelopts('opaquemodules=false');
if not p::procedure then
error "%1 is not a procedure", p
else
return eval(p)
end if;
finally
kernelopts('opaquemodules' = opacity);
end try;
end proc:
Here is the version of GetInert that uses GetProc.
GetInert4 := proc(p::uneval)
ToInert(GetProc(p))
end proc:
Now for the tests
GetInert4(Test[f]);
_Inert_PROC(_Inert_PARAMSEQ(_Inert_NAME("x")), _Inert_LOCALSEQ(), _Inert_OPTIONSEQ(), _Inert_EXPSEQ(),
_Inert_STATSEQ(_Inert_POWER(_Inert_PARAM(1), _Inert_INTPOS(2))), _Inert_DESCRIPTIONSEQ(),
_Inert_GLOBALSEQ(), _Inert_LEXICALSEQ(), _Inert_EOP(_Inert_EXPSEQ()))
GetInert4(Test[g]);
Error, (in GetProc) module does not export `g`
GetInert4(g);
Error, (in GetProc) g is not a procedure
Comments
Tools of the Masters
A link to this post probably belongs in the 'tools of the masters' book.
a hack job
Okay; I'll have to come up with a useful title. The try statement being straightforward, I originally considered this tip as more suited for inclusion in the Tools for the Reasonably Competent book; however, I think the elimination of the assignment to the inert variable is a nice flourish. I saw that in library code this weekend; previously, I've utilized some ugly hacks to avoid an extra assignment (those tips belong in another wing of the library).