RuleDelayed
is unconventional in that it strips any number of occurrences of Unevaluated
from the right-hand side:
a :> Unevaluated[1 + 1]
(* a :> 1 + 1 *)
a :> Unevaluated[Unevaluated[Unevaluated[1 + 1]]]
(* a :> 1 + 1 *)
This is different from the normal evaluation process which only strips one level:
ClearAll[f]
f[x_] := x
f[Unevaluated[Unevaluated[Unevaluated[1 + 1]]]]
(* Unevaluated[Unevaluated[1 + 1]] *)
I speculate that this non-standard behaviour is intended to simplify the semantics of rule application. Since Unevaluated
has special meaning to the evaluator, it is hard to pattern-match (as we shall see below). I think that the brute-force strategy of stripping all levels of Unevaluated
makes it easier to reason about the most common cases of rule application.
We might try to reproduce this behaviour naively as follows:
SetAttributes[ruleDelayed, {HoldRest, SequenceHold}]
ruleDelayed[u_, HoldPattern[Unevaluated[x_]]] := ruleDelayed[u, x]
But this always leaves one left-over Unevaluated
:
a ~ruleDelayed~ Unevaluated[1 + 1]
(* ruleDelayed[a, Unevaluated[1 + 1]] *)
a ~ruleDelayed~ Unevaluated[Unevaluated[Unevaluated[1 + 1]]]
(* ruleDelayed[a, Unevaluated[1 + 1]] *)
The reason is that Unevaluated
gets special treatment from the evaluator. It is stripped before the containing expression is evaluated, but is restored if there are no replacement rules applicable to that expression:
ClearAll[f]
f[x_] := Null /; (Print[Hold[x]]; False)
f[Unevaluated[1 + 1]]
(*
Hold[1 + 1]
f[Unevaluated[1 + 1]]
*)
Notice how the body of f
saw the argument without Unevaluated
. But the Unevaluated
wrapper has been restored in the return value when the definition was rendered inapplicable by the False
condition.
Since Unevaluated
is unwrapped prior to the application of definitions, we can never pattern-match that outermost Unevaluated
. (Never, that is, without resorting to HoldComplete
-- but RuleDelayed
does not use that.)
So what we need is to add a definition to our ruleDelayed
that:
- is applicable to any input (to cause
Unevaluated
to be stripped but not restored),
- returns that input unchanged,
- and does not cause infinite recursion.
A tall order, but not impossible:
Module[{blocked = False}
, ruleDelayed[u_, x_] /; !blocked := Block[{blocked = True}, ruleDelayed[u, x]]
]
With this additional rule, we can reproduce the desired behaviour:
a ~ruleDelayed~ (1 + 1)
(* ruleDelayed[a, 1 + 1] *)
a ~ruleDelayed~ Unevaluated[1 + 1]
(* ruleDelayed[a, 1 + 1] *)
a ~ruleDelayed~ Unevaluated[Unevaluated[Unevaluated[1 + 1]]]
(* ruleDelayed[a, 1 + 1] *)
The extra definition might work, but it is somewhat brittle. The use of a dynamically-scoped variable to conditionally inhibit the operation of the definition can be defeated in pathological conditions (for example, up-value evaluations that sneak into the inner call). While this is very unlikely to happen in practice, a more bullet-proof solution would somehow trigger the stripping of Unevaluated
more directly.
RuleDelayed
appears to achieve this "more bullet-proof" solution through built-in definitions. Note that RuleDelayed
has built-in "down code":
System`Private`HasDownCodeQ[RuleDelayed]
(* True *)
I suspect that this hard-coded definition directly tells the evaluator not to restore a stripped Unevaluated
head, analogous to our magic Module[...]
definition.
Furthermore, RuleDelayed
has at least one built-in down-value:
DownValues[RuleDelayed]
(* False *)
System`Private`HasDownEvaluationsQ[RuleDelayed]
(* True *)
My guess is that this parallels our definition that strips all levels of Unevaluated
but the first.