Prevent graphics from rendering inside a held expression
Recall that the rendering of Graphics
has nothing to do with evaluation. It is done entirely in typesetting. And therefore, a robust solution will treat this as a problem of typesetting, and not as a problem of evaluation.
Once you frame the problem properly, the solution is fairly straightforward. What you want to do is to change the typesetting of Hold
(and friends). Take a look at this:
Unprotect[Hold];
Hold /: MakeBoxes[Hold[expr_], fmt : StandardForm | TraditionalForm] :=
Block[{Graphics, Graphics3D}, Unprotect[Graphics, Graphics3D];
Clear[Graphics, Graphics3D];
RowBox[{"Hold", "[", MakeBoxes[expr, fmt], "]"}]]
Protect[Hold]
Fortunately, Hold
(and HoldForm
and HoldComplete
) has no typesetting rules directly attached to it that you're fighting, which you can determine using FormatValues[Hold]
. But Graphics
and Graphics3D
do; it's how typesetting of graphics works at all. We want to suppress those rules, but only within the typesetting of Hold
. So we use Block
to contain the damage we're about to do to the Graphics
and Graphics3D
symbols, and then use Clear
to clear them. From there on out, we let MakeBoxes
do what it would normally do.
Note that this example cheats a bit; it only works if you pass one argument to Hold
. I did that for purpose of code simplicity and illustration. To make the formatting rules work properly for Hold[expr___]
, I would have to write multiple and more sophisticated rules, or I would have to use the Villegas-Gayley trick.
Edit: As came up in the comment discussion, it really isn't necessary to Unprotect
and Clear
the symbols Graphics
and Graphics3D
, as Block
is effectively doing that already. I've considered editing the code to make it shorter/simpler, but perhaps the existing code is clearer for people who don't fully understand how Block
works (and, public confession here, while I understand Block
scoping, I had just plumb forgotten how Block
initializes variables, so this more an oversight on my part than a planned teaching moment).
John Fultz alluded to using the Villegas-Gayley pattern. Since I believe that is the correct approach to this problem here is an implementation.
mk : MakeBoxes[(Hold | HoldForm | HoldComplete | HoldPattern)[__], _] :=
Block[{$hldGfx = True, Graphics, Graphics3D}, mk] /; ! TrueQ[$hldGfx]
I included HoldPattern
to complete the Hold functions. This now works at any depth:
Hold[1, 2, foo[3, Graphics[{Green, Circle[]}], 4], 5]
Hold[1, 2, foo[3, Graphics[{Green, Circle[]}], 4], 5]
Overhead
Jacob Akkerboom questioned the overhead of this general rule attached to MakeBoxes
. To test this I converted a large expression to Box form using ToBoxes
(which calls MakeBoxes
), and timed the operation with and without this definition as well as several alternatives. Here are the results (in version 7). Each test was performed in a fresh kernel, using this code:
expr = Expand[(1 + x + y)^4 (2 - z)^5 (q - 7 - a)^7 (b + r - 4)^6];
ToBoxes[expr] // AbsoluteTiming // First
- Raw (no additional
MakeBoxes
rules): 0.7940454 Alternatives
DownValue onMakeBoxes
: 1.1030631- Four individual DownValues on
MakeBoxes
: 1.4690841 - Four UpValues on Hold* functions: 0.7890451
(Incidentally, use of the Notations Package causes far larger overhead; the timing performed in my standard configuration was 3.5652039 seconds.)
It appears that Jacob's concern is valid as the overhead of my method above is significant, though not extreme. I usually attach rules to MakeBoxes
to avoid having to Unprotect
system Symbols but I may have to reconsider that practice. If you prefer unprotecting system Symbols to the overhead you may use this:
(
Unprotect @ #;
mk : MakeBoxes[Blank[#], _] /; ! TrueQ[$hldGfx] ^:=
Block[{$hldGfx = True, Graphics, Graphics3D}, mk];
Protect @ #
) & ~Scan~ {Hold, HoldForm, HoldComplete, HoldPattern}
This is not an answer but an extended comment.
Regarding your assertion
This occurs because although the front end attempts to render the Graphics element the internal code won't replace Directives inside of a Held expression.
This is not the case. Consider
Hold[Graphics[{RGBColor[1, 0, 0], Thick, Circle[]}]]
and
With[{red = Red}, Hold[Graphics[{red, Circle[]}]]]
So the error message comes from using the built-in symbol Red
and not from the front end doing anything funny with directives.