Closed kubaPod closed 5 years ago
I'm a bit sceptical about making this change. I think this needs more thought.
Consider
var (* 1 *)
Block[{var (* 2 *)},
var (* 3 *)
]
What was your intention?
We should also write down concrete use cases (perferably not just made-up ones, but something that provably exsits in the wild).
Block
is often used for trivial, Module
-like localization a lot. I use it like this in IGraph/M. The performance increase compared to Module
is measurable because the inside of Block
has a (very fast) LibraryLink function. I see this use a lot in WRI-written code too. It's not a problem for as long as these variables only get numerical values (and never any symbolic values passed from the outside). Having the local variable reside in the package-context also increases safety.
Example:
f[x_?NumericQ] := Block[{x}, x+=1; x]
For this use case, the current behaviour is ideal and anything else would break code.
Then there the use case when Block
is used to set temporary values to global symbols. I use this extensively in LTemplate:
https://github.com/szhorvat/LTemplate/blob/master/LTemplate/LTemplateInner.m#L253
Example:
current (* is a global variable *)
main[x_, y_] :=
Block[{current},
current = getName[x];
process[y];
]
Here main
wants to provide some context about what data is being processed to several sub-functions that it is calling. Not having to pass in all this information to the sub-functions as explicit arguments makes the code much more maintainable. Thus main
sets this information temporarily on a global variable, which all the callees can access.
In practice, when LTemplate is processing an expression representing a class interface like
LClass["MyClass", {
LFunction["myfun", ...]
}]
then if it encounters an error while looking at myfun
, it want to be able to report a user-friendly error that mentions which class myfun
was localted in (in this case, MyClass
). This contextual and chaning information is passed down through termpoarily-modified global variables.
For this use case, global renaming would be useful.
Implement something like Table
:
SetAttributes[table, HoldFirst];
table[expr_, {var_, s_, t_}] :=
Block[{var},
Function[var = #; expr] /@ Range[s, t]
]
This is quite different from Use Case 2 because var
in the Block
is the same as var
in the function. Handling this correctly as well requires a smarter approach. The var
in Block
should be the same as the var
in the pattern, but not the same as a global var
.
I think this illustrates how complex this issue is well.
BTW this exact this is why I did not request that Internal`LocalizedBlock
should be handled too. A function like table
above would usually use LocalizedBlock
. The present behaviour of the IDE with
SetAttributes[table, HoldFirst];
table[expr_, {var_, s_, t_}] :=
Internal`LocalizedBlock[{var},
Function[var = #; expr] /@ Range[s, t]
]
is actually better than if LocalizedBlock
would be handled as a localizing construct (similar to Module
/Block
).
My personal opinion on the matter is that Block
should be left as it is, for the following reasons:
There is a conflict between Use Case 1 and Use Case 2. Both can't be handled well as the IDE can't know the programmer's intent. Currently, Use Case 1 is handled well. I believe that it is more common in package code than Use Case 2 (I admit that this is based on anecdotal evidence and I may be wrong).
Use Case 2 can easily be handled with a file-global Find and Replace. Just set Match case
and Words
. Furthermore, I can explicitly control if I want file-global or project-global replacement.
Refactoring/renaming is inherently dangerous because there are always cases where the IDE can't possibly decide what is the correct behaviour. There is always a chance for things to go wrong. Refactoring would work across files in the same project. If we prioritize Use Case 2, a mistake/misunderstanding will lead to much more damage than if we prioritize Use Case 1 (which only does a very local replacement, that is easier to verify).
Use Case 1 is simpler and what people would naively expect. (It is also common.) I think it's good design to follow the more naïve expectations and avoid user surprise when possible. (If the user's expectation is plainly wrong, that's a different matter. But here local replacement is a very reasonable expectation).
Use Case 3, plus my first comments about the different possible implementations, show that this is a very complex issue, that's hard to get right. I am sure that there are more special cases where the change wouldn't achieve the intended effect anyway. We just have not thought of it yet.
And of course, because of this, implementation would take a lot of time that could be better spent.
I am in favour of maintaining the current behaviour. It's not perfect, but clearly useful.
So,
Should Block variables be renamed by global 'Rename'?
My vote is no.
@kubaPod Let me know if I misunderstood what you were asking for.
I agree with the Szabolcs. From the developer's point of view, implementing something like this would require a lot of changes for a very small gain which doesn't even represent the usual use-case.
When renaming variable
$whatever
with Shift+F6 everything goes well butBlock[{$whatever = 1}, ...
will stay intact.I think both approaches, to and not to rename it, have pros and cons but somehow I feel that renaming it will be more beneficial in future.
That of course means that
$whatever
inside theBlock
would need to be replaced as well. Now it is shielded.Shortly, should expressions that implement dynamic scoping be transparent to 'Rename'?
If so, that applies to
Internal`InheritedBlock
as well.