Scoped SetDataFolder calls
741
That means that I often find myself doing the following:
DFREF savDF = GetDataFolderDFR()
SetDataFolder root:Packages:myFolder
ImageTransform /P=0 getPlane, someWave // just an example
// ImageTransform doesn't allow me to tell where the wave is to be made,
// so it forces me to use explicit SetDataFolder calls
if (weShouldStopHere)
SetDataFolder savDF // reset the active data folder to whatever it was so the user doesn't get confused
return 0
endif
ImageTransform /P=1 getPlane, someWave // make some more waves
SetDataFolder savDF // also reset the active folder here
return 0
SetDataFolder root:Packages:myFolder
ImageTransform /P=0 getPlane, someWave // just an example
// ImageTransform doesn't allow me to tell where the wave is to be made,
// so it forces me to use explicit SetDataFolder calls
if (weShouldStopHere)
SetDataFolder savDF // reset the active data folder to whatever it was so the user doesn't get confused
return 0
endif
ImageTransform /P=1 getPlane, someWave // make some more waves
SetDataFolder savDF // also reset the active folder here
return 0
99.999% of the time I find SetDataFolder and friends to be obnoxious, and feel that they just clutter my code. Imagine if there was a way to have a scoped data folder call:
SetScopedDataFolder root:Packages:myFolder
// SetScopedDataFolder promises that the active data folder will be reset
// to whatever it was before it was called
// when we leave the current scope
ImageTransform /P=0 getPlane, someWave // just an example
if (weShouldStopHere)
return 0
endif
ImageTransform /P=1 getPlane, someWave // make some more waves
return 0
// SetScopedDataFolder promises that the active data folder will be reset
// to whatever it was before it was called
// when we leave the current scope
ImageTransform /P=0 getPlane, someWave // just an example
if (weShouldStopHere)
return 0
endif
ImageTransform /P=1 getPlane, someWave // make some more waves
return 0
How's that for boilerplate avoidance? The functionality is the same, the code is clearer, and more robust.
In a lot of cases I can attempt to make sure that the active data folder is reset by covering every exit path with a SetDataFolder call. But occasionally it's impossible to do so if an operation throws an error. Consider the following:
DFREF savDF = GetDataFolderDFR()
SetDataFolder root:Packages:myFolder
SomeOperationThatThrowsAnError
SetDataFolder savDF // I'm screwed, control never reaches here
return 0
SetDataFolder root:Packages:myFolder
SomeOperationThatThrowsAnError
SetDataFolder savDF // I'm screwed, control never reaches here
return 0
There's no straightforward way to reset the active data folder before we crash! It's possible if I get really kludgy and start using GetRTError and the like. But honestly, that's just a shame.
So, please, WaveMetrics, provide SetScopedDataFolder for us to play with. I'm tired of sprinkling SetDataFolder calls all over my code just because I don't like loose ends. And even then I can't catch everything by the very nature of Igor's design.
I find it useful to create a routine that creates my private data folder and another that returns a reference to it, like this:
NewDataFolder/O root:Packages
NewDataFolder/O root:Packages:MyPackage
End
Function/DF GetPrivateDFR()
DFREF dfr = root:Packages:MyPackage
return dfr
End
Function CreateMyPrivateData()
DFREF dfr = GetPrivateDFR()
Make/O dfr:myWave = 0
Variable/G dfr:myVariable = 0
End
Function UseMyPrivateData()
DFREF dfr = GetPrivateDFR()
Wave myWave = dfr:myWave
NVAR myVariable = dfr:myVariable
End
For details, execute this:
November 22, 2010 at 12:38 pm - Permalink
I assume that you're referring to a consistent use of dfref:something, e.g. in Make and WAVE calls. I already try to do this as much as possible, even going to the extent of using DataFolderAndName parameters in my XOPs.
But it's still impossible to avoid the use of SetDataFolder entirely. Some examples that come to mind are ImageTransform, MatrixLLS, and probably MatrixOP, as well as a significant number of others. I'm aware of the techniques that you refer to, but even with them the setdatafolder calls remain a kludge.
November 22, 2010 at 01:11 pm - Permalink
String MyPath = "root:Packages:MyPackage:"
String MyWave = "MyWave"
String sDestinationWave
sDestinationWave = MyPath + MyWave
WAVE MyImageWave = $(MyPath + "MyImageWave")
imageHistogram /P=0 MyImageWave
Duplicate /O W_ImageHist, $sDestinationWave
KillWaves /Z W_ImageHist
End
November 22, 2010 at 03:40 pm - Permalink
Well this is certainly a way to avoid changing the data folder unintentionally. But let's be honest: it's still a kludge that exists because there is no way to have a scoped SetDataFolder call. I think we can all agree that these kludges reduce code readability, place an extra burden on the programmer, and increase complexity.
November 23, 2010 at 01:22 pm - Permalink
November 24, 2010 at 07:32 am - Permalink
To make this less painful, I usually create a simple routine that takes a name and returns the full path, but sometimes use a SetDF-type routine to isolate the exact data folder path to one or two routines (in case I change my mind):
String name
NewDataFolder/O root:Packages
NewDataFolder/O root:Packages:myDataFolder
return "root:Packages:myDataFolder:"+PossiblyQuoteName(name)
End
// If I need to change to the data folder, I use this:
Function/S SetMyDF()
String oldDF= GetDataFolder(1)
NewDataFolder/O root:Packages
NewDataFolder/O/S root:Packages:myDataFolder
return oldDF
End
// Example
Function doSomething(inputWaveinRoot)
Wave inputWaveinRoot
// Duplicate the input and normalize it to max = 1
String out= PathInDF("output")
Duplicate/O inputWaveinRoot, $out
Wave wout= $out
WaveStats/Q wout
wout /= V_Max
// Example: create a global variable in my data folder
Variable/G $PathInDF(name) = 1 // initialize
NVAR nv=$PathInDF(name) // to use or alter the global
// show how to switch to DF and back
String oldDF= SetMyDF()
// do stuff
SetDataFolder oldDF
// etc
End
--Jim Prouty
Software Engineer, WaveMetrics, Inc.
November 24, 2010 at 09:11 am - Permalink
So I think the real solution is to add a /DEST flag to operations that lack them, such as ImageTransform.
Pending that, for those operations that lack /DEST or the equivalent, I think I would create a wrapper function that uses jtigor's idea (Duplicate/O followed by KillWaves/Z) to emulate the /DEST flag. This would allow you to use the DFREF technique throughout the rest of your code.
November 24, 2010 at 10:40 am - Permalink
The Duplicate technique is more robust, but carries with it the overhead of copying the data, and also adds code complexity.
The /DEST flag sounds like a more fundamental solution. However, a significant number of operations create more than one wave (e.g. funcfit, MatrixLLS, ...), which means that the /DEST flag would be inadequate. An option in that case would be a /DF flag that allows one to pass in a DFREF. But there's plenty of possibilities for confusion, and overall it seems like a lot of operations would have to be changed.
So I still consider SetScopedDataFolder the best solution. With the addition of free data folders and free waves, scoped entities are already available in Igor, so it would not be unprecedented. It's easy on the programmer, robust, unobtrusive, doesn't clutter the code, and does not require any copying overhead. Furthermore there would be no need to to modify the existing operations.
November 25, 2010 at 02:15 am - Permalink
SomeOperationThatThrowsAnError
as an example, but obviously that isn't a real operation. So what operation(s) cause problems for you? Many operations accept a /Z flag which allows the programmer to handle any errors produced by the operation. I recommend using the /Z flag with the NVAR, SVAR, and WAVE variable types as well and then check that the variable exists (NVAR_Exists(), WaveExists(), SVAR_Exists()).You could also put your code that might produce run time errors within a try...catch...endtry construct. You may also need to append
;AbortOnRTE
to the end of statements with operations that may produce errors but which don't accept the /Z flag. Here is an example of how you would use this construct:Function test()
Variable weShouldStopHere
WAVE someWave = someWave
DFREF savDF = GetDataFolderDFR()
try
NewDataFolder/O root:Packages
NewDataFolder/O/S root:Packages:myFolder
ImageTransform /P=0 getPlane, someWave;AbortOnRTE // just an example
// The next line will cause an abort if weShouldStopHere is true.
AbortOnValue weShouldStopHere, kAbortButNoError
ImageTransform /P=1 getPlane, someWave;AbortOnRTE // make some more waves
catch
// You may want to clear the error code, like so.
Variable errorCode = GetRTError(1)
if (V_AbortCode != kAbortButNoError)
// You may need to do stuff here if you want
// to do extra error handling.
endif
endtry
// As long as you don't have any return statements above,
// execution should always reach here. So the data folder
// always gets reset.
SetDataFolder savDF // also reset the active folder here
return 0
End
November 25, 2010 at 09:16 am - Permalink
where I find myself missing scoped data folders the most is in a package that includes both I/O as well as fairly complex calculations, which can naturally cause runtime errors. In this particular project XOP operations play a big role, however, the problem is the same for Igor's operations as well.
For what it's worth, I did add /Z flags to these operations. However, an important aspect is that I do not wish to suppress the error by any means. The error happened, it's significant, and the user should be notified about it right away. I could go about handling the error with try/catch and/or GetRTError, but I still want an alert dialog. Moreover, the meaning of the different error codes should not be hardcoded in the procedure file since this makes the code harder to maintain. But I need to get the exact error message so I can display an abort panel! Easy, right? GetErrMessage() to the rescue! Except that it doesn't recognize any XOP-specific error messages, which defeats the purpose.
So in the end I've just complicated my code even more to get something that approaches robustness (but requires other sacrifices). Also note that I'm still responsible for covering every code path with SetDataFolder calls, so I haven't made much progress either way.
Anyway, I've been doing some more thinking on them:
SetScopedDataFolder root:Packages:myPackage
// active data folder is now root:Packages:myPackage
// do some stuff here
// if an error or return statement occurs here then we'll
// be back in whatever data folder was set before
level2()
// level2 changes to another data folder
// but it's scoped to be local to level2 so we don't care
// whatever happens we're back in root:Packages:myPackage when we reach this line
SetScopedDataFolder root:Packages:myPackage2
// now we're in root:Packages:myPackage2
// when the function goes out of scope we'll go back to the original data folder
// but possibly passing through root:Packages:myPackage, which is transparent to the user
End
Function level2()
SetScopedDataFolder root:someFolder
// we're now in root:someFolder
// this function is called from level1(), so when we leave this scope
// the folder should be reset to root:Packages:myPackage
End
Function PotentialProblem()
SetScopedDataFolder root:hello
// do something
SetDataFolder root
// There's a possible problem here, though it's likely a corner case
// the user wants to change the active 'static' data folder
// the best way to handle it seems for Igor to look at the current stack
// of scoped data folder calls and to modify the highest-level one (that will go out of scope last)
// so it will change the active DF to root when it goes out of scope (i.e. when we stop executing a user function)
// the idea here is that there is a distinction between changing the active data folder
// for the user (globally) and locally within a function
End
November 25, 2010 at 01:27 pm - Permalink
I also second this feature request.
Coming back to the original question of a SetScopedDataFolder function, I think this is not the correct solution for the problem. The standard way of how XOP runtime errors are handled is not very programmer friendly in my eyes. Having a dialoge popup by default is not what I expect if I call an XOP operation from a user defined function.
Adding support for the return value of an XOP and its error message to be read out by GetErrMessage(), and therefore turning off the automatic dialoge popping up, would greatly reduce the cases where a "SetDataFolder $privateFolder" is umatched.
Slightly offtopic:
I've choosen for a XOP I'm currently coding a similiar solution for error handling. The return state of the operation is (as number) returned in a V_flag variable. The coresponding error message can be retrieved by MFR_GetXOPErrorMessage as string.
In the procedures this looks like:
int32 SUCCESS
int32 ALREADY_FILE_OPEN
int32 FILE_NOT_READABLE
[...]
EndStructure
Function initStruct(errorCode)
Struct errorCode &errorCode
errorCode.SUCCESS =0
errorCode.ALREADY_FILE_OPEN=10002
errorCode.FILE_NOT_READABLE=10008
[...]
end
Function mytest()
Struct errorCode errorCode
initStruct(errorCode)
MFR_OpenResultFile
if(V_flag != errorCode.SUCCESS)
MFR_GetXOPErrorMessage
print S_errMsg
endif
[...]
End
I must admit it still looks a bit clumsy because of the required initStruct() call. As all my XOP operations always end with "return 0;" I don't have to fear that they stop at an unexpected moment and I still know how they exited.
December 3, 2010 at 04:54 am - Permalink
XOP errors are handled the same as Igor errors. In both cases, the default is to stop procedure execution and display a dialog. In both cases, the Igor programmer can suppress the dialog and allow execution to continue using GetRTError(1). In both cases, GetRTErrMessage returns the error string. Try executing this:
GBLoadWave /P=Igor /F=-1 "License Agreement.txt"
String errMessage = GetRTErrMessage()
Variable err = GetRTError(1)
Print err, errMessage
Print GetErrMessage(err)
End
The function result from an external operation or external function C routine, if non-zero, is intended to signify a normally fatal error that should stop procedure execution. You can make it non-fatal using GetRTError(1).
If you want to return status information, as opposed to a normally fatal error, use something other than the function result such as a V_ variable for an external operation or, for an external function, the p->result field or a pass-by-reference parameter.
December 3, 2010 at 09:06 am - Permalink
I can't say I agree with this. My definition of an error is an event or circumstance that causes the result of the operation to be invalid. In my opinion displaying a popup is entirely appropriate in this situation.
However, this topic has sort of drifted away from my original intent, which is that the SetDataFolder spaghetti is annoying. The error handling just popped up as an extreme situation where the spaghetti fails even if set up properly.
So let me just summarize my points again:
1. The current SetDataFolder handling is tedious and unpleasant. Examples of this can be found in my own code but also in WaveMetrics procedures.
2. In over five years of daily Igor use and development I have have never wanted to change the data folder globally from a procedure.
3. The way Igor is designed currently allows the use of SetDataFolder to be reduced, but not abolished.
In this thread we've highlighted two possible solutions to avoid the spaghetti:
1. A scoped SetDataFolder call.
2. Adding /DEST and/or /DF flags to every operation.
My preference is still with scoped SetDataFolder calls. It's elegant, bullet-proof (if implemented properly in Igor), and will do away with the SetDataFolder cluttering.
To convince you that this cluttering happens to the best, I submit the following code from MultiPeakFit in Igor 6.21 (some comments removed):
if (!DataFolderExists(resultDF))
return MPF2_Err_NoDataFolder
endif
SetDataFolder resultDF
if (nBLParams > 0)
Wave w = $BLCoefWaveName
if (!WaveExists(w))
SetDataFolder dataFolderPath
return MPF2_Err_BLCoefWaveNotFound
endif
if (numpnts(w) != nBLParams)
SetDataFolder dataFolderPath
return MPF2_Err_BLCoefWrongNPnts
endif
endif
// This loop counts the peaks as it checks the length of the coefficient waves
nPeaks = 0
do
sprintf cwavename, PeakCoefWaveFormat, nPeaks
Wave w = $cwavename
if (!WaveExists(w))
break;
endif
if (numpnts(w) != nPeakParams)
SetDataFolder dataFolderPath
return MPF2_Err_PeakCoefWrongNPnts
endif
nPeaks += 1
while(1)
if (nPeaks == 0)
SetDataFolder dataFolderPath
return MPF2_Err_PeakCoefWaveNotFound
endif
4 out of those 5 calls would be unnecessary with scoped SetDataFolders.
December 4, 2010 at 09:54 am - Permalink
The multiple calls to SetDataFolder saveDF could be eliminated by either:
1) using full paths (which have to be constructed in a string) with the Wave lookups.
2) using a DFREF variable to eliminate constructing that string.
That is, replace
with either
or
...
Wave w = df:$BLCoefWaveName
That code you copied is from a much larger function; I would have to look it over carefully to make sure that none of the called functions expects the current data folder to be set to the resultDF, and I would have to go through all the code of the function to see if later code expects the data folder to be set in a particular way. But the code you copied can be cleaned up using these techniques. In my own defense, DFREF didn't exists when I wrote that code :)
John Weeks
WaveMetrics, Inc.
support@wavemetrics.com
December 6, 2010 at 12:39 pm - Permalink
Thanks for these clarifications Howard.
December 8, 2010 at 01:55 am - Permalink