REWRITE: Multiple edits were hurting clarity
Overview:
I'm doing some work involving injecting code into held expressions, and found that I was repeating myself in a couple areas related to the available Replace
and ReplacePart
syntaxes. I'll call the two patterns I found myself repeating "ReplaceThen
" and "ReplaceAt
".
Below, I define ReplaceThen
and ReplaceAt
, including:
- Definition
- Minimal example
- Constraints
- Motivation (why do I want this?)
After that, I've included the code for my first versions of ReplaceThen
and ReplaceAt
.
Request:
I'm looking for some feedback (this post is a request-for-comment) on my first versions of ReplaceThen
and ReplaceAt
. I'm a bit of a newbie, and I want to make sure that:
- I considered all the edge-cases, and my code is "functionally correct" in the general case
- Performance is reasonably-close to optimal, for code written in native WFL.
- Pattern guards are effective without being overly-onerous
- Flexibility is sufficient enough for functions intended to cover general-case use -- do you have any use-cases you run into yourself that aren't currently solved, but could be with reasonable modifications? Do you have any use-cases that might motivate me to create a 3rd/N'th function within this family of
Replace
extensions? - Forget about the "..., for code written in native WFL" restriction above. Might these warrant being written in C? My guess is that since
Replace
,ReplacePart
,Extract
, andDispatch
are all heavily-optimized already, it's probably not worth writing everything from scratch.
This post is a request-for-comment (RFC) on these two functions in-general, with a specific emphasis on the above questions.
Function 1: ReplaceThen
Definition:
Perform a normal Replace
, but allow wrapping the result in an arbitrary head after replacement, but before any subsequent evaluation.
Minimal example - input:
ReplaceThen[Unevaluated[1+1],1->2,{-1},Hold]
Minimal example - desired output:
Hold[2+2]
c.f.: Replace[Unevaluated[1+1], 1->2, {-1}]
would return 4
Constraints
Any techniques used to implement
ReplaceThen
(such as additions ofHold
s) should be transparent to the end-user. For example, if I callReplaceThen[Unevaluated[1+1],Plus->Times,{1},Hold,Heads->True]
, my level specification of{1}
should be interpreted exactly the same way that regularReplace
interprets its level specifications.Any techniques used to implement
ReplaceThen
(such as addition ofHold
s) should be "bulletproof" in that no rules supplied by the end-user should be capable of "breaking" the function. Specifically,ReplaceThen
must support edge-cases where the end-user deliberately supplies rules which transformHold
,HoldComplete
, or an arbitrary head such asBlank[]
.
Motivation:
Conveniently, Extract
offers us an optional third parameter where we can wrap the extracted contents in an arbitrary head before evaluation. From the documentation:
Extract[expr,pos,h]
extracts parts of expr, wrapping each of them with head h before evaluation.
Unfortunately, Replace
does not offer the same convenience argument out-of-the-box. The reason for creating ReplaceThen
which supports a "wrap in head h
" argument is the same as the reason for supporting a "wrap in head h
" argument for Extract
: passing the value returned from Replace
to a downstream function is not the same as performing a replacement, then wrapping the replaced expression in an arbitrary head prior to evaluation. It only makes a difference if the replaced expression would evaluate to something else in the first place, but that is the entire subject of this post.
Bottom line:
This is not a complicated function, and instead of:
ReplaceThen[Unevaluated[1+1],Plus->Times,{1},Hold,Heads->True]
If you were only performing a one-off case, you could certainly directly perform:
Replace[Hold[1+1], Plus->Times, {2}, Heads->True]
(just adjust level specification from {1}
to {2}
in this example)
To re-emphasize: the entire point of this function is to handle the general-case correctly (with respect to the above constraints) and conveniently for arbitrary user-provided rules, so that you don't have to choose between technical correctness and verbosity.
Function 2: ReplaceAt
Definition:
Perform a normal Replace
(including arbitrary pattern rules, not just substitution of one part with a verbatim-substituted replacement part like ReplacePart
does), but only on a portion of an expression specified by an arbitrary part specification (not just a level specification like Replace
takes).
Minimal example - input:
ReplaceAt[Hold[1+1,1+1],1->2,{2},{-1}]
Minimal example - desired output:
Hold[1+1,2+2]
c.f.: Replace[Hold[1+1,1+1],1->2,{-1}]
would return Hold[2+2,2+2]
, and Hold[1+1,Evaluate@Replace[Unevaluated[1+1],1->2,{-1}]]
would return Hold[1+1,4]
Constraints
Same as ReplaceThen
:
Interpret a level specification of e.g.
{1}
exactly the same way thatReplace
does, regardless of whether the implementation includes e.g. an internalHold
. Interpret a part specification of e.g. {1} exactly the same way thatExtract
does.Be bulletproof to arbitrary user-supplied rules, including the edge-cases noted for
ReplaceThen
.
Motivation:
The reason for creating an extension of Replace
accepting a part specification argument, versus just using MapAt
on Replace
(or a closure containing it) is the same reason ReplacePart
exists, versus just mapping newExpr&
to some part of an expression: it performs the replacement prior to evaluation or without evaluation of the larger expression.. Remember, the semantics of Replace
are that replacements are performed "...even when parts have Hold or related wrappers" (including HoldComplete
or functions with HoldAllComplete
attributes), so even using MapAt[ReplaceAll[rules], ...
followed by MapAt[Evaluate, ...]
would not provide these semantics.
The code follows below...:
First, some boilerplate: setup a HoldComplete
-like wrapper, and some dispatch tables for modifying user-provided rules and level specifications:
ClearAll[ReplaceAt`HoldComplete, partSpecPatt, replaceRulePatt,
levelSpecPattern, ReplaceThen, ReplaceAt]
(* Surrogate for HoldComplete: in case rules themselves transform HoldComplete *)
SetAttributes[ReplaceAt`HoldComplete, HoldAllComplete]
(* Boilerplate pattern guards *)
partSpecPatt = _Integer | _Span |
All | {partSpecElements___ /; (And @@
Thread@Unevaluated@MatchQ[{partSpecElements}, partSpecPatt])};
replaceRulePatt = _Rule | _RuleDelayed | {(_Rule | _RuleDelayed) ..};
levelSpecPattern = _Integer | All |
Infinity | -Infinity | {Repeated[_Integer |
Infinity | -Infinity, {1, 2}]};
(* Translate arbitrary rule(s) to never modify ReplaceAt`HoldComplete *)
(* e.g. if the rule was _[x_]:>Identity[x] *)
ruleExceptTransform =
Dispatch[(h : Rule | RuleDelayed)[l_, r_] :>
h[Except[ReplaceAt`HoldComplete | _ReplaceAt`HoldComplete, l], r]];
(* Translate level spec to account for ReplaceAt`HoldComplete *)
levelSpecTransform =
Dispatch[{{n1_, n2_Integer?Positive} :> {n1, n2 + 1},
n_Integer?Positive :> n + 1, {n_Integer?Positive} :> {n + 1}}];
(* Sugar: make user calls to Options[...] return something *)
(* Allow specifying attributes, e.g. HoldFirst, for Operator forms*)
Options[ReplaceThen] =
Join[Options[Replace], {OperatorHold -> {SequenceHold}}];
Options[ReplaceAt] =
Join[Options[Replace], {OperatorHold -> {SequenceHold}}];
SetAttributes[ReplaceThen, SequenceHold]
SetAttributes[ReplaceAt, SequenceHold]
Next, here are ReplaceThen
and ReplaceAt
:
With[{ruleExceptTransform = ruleExceptTransform,
levelSpecTransform = levelSpecTransform},
ReplaceThen[expr_, rules : replaceRulePatt,
levelSpec : levelSpecPattern : {0},
wrap_Symbol : Function[x, x, SequenceHold],
opts : OptionsPattern[ReplaceThen]] := wrap @@ Replace[
ReplaceAt`HoldComplete[expr],
Replace[rules, ruleExceptTransform, {0, 1}],
Replace[levelSpec, levelSpecTransform],
FilterRules[{opts}, Options[Replace]]
];
ReplaceAt[expr_, rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0},
opts : OptionsPattern[ReplaceAt]] :=
ReplacePart[
Unevaluated@expr,
Replace[
Replace[
Extract[Unevaluated@expr, partSpec, ReplaceAt`HoldComplete],
Replace[rules, ruleExceptTransform, {0, 1}],
Replace[levelSpec, levelSpecTransform],
FilterRules[{opts}, Options[Replace]]
],
ReplaceAt`HoldComplete[x_] :> RuleDelayed[partSpec, x]
]
];
]
We might as well support Operator forms of the above, while we're at it:
ReplaceThen[rules : replaceRulePatt,
levelSpec_ : levelSpecPattern : {0},
wrap_Symbol : Function[x, x, SequenceHold],
opts : OptionsPattern[ReplaceThen]] :=
Function[expr, ReplaceThen[expr, rules, levelSpec, wrap, opts],
Evaluate@OptionValue@OperatorHold] /. DownValues@ReplaceThen /.
Verbatim[FilterRules][x_, y__] :> RuleCondition@FilterRules[x, y];
(* Remember Unevaluated if not inlining with DownValues *)
ReplaceAt[rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0},
opts : OptionsPattern[ReplaceAt]] :=
Function[expr, ReplaceAt[expr, rules, partSpec, levelSpec, opts],
Evaluate@OptionValue@OperatorHold] /. DownValues@ReplaceAt /.
Verbatim[FilterRules][x_, y__] :> RuleCondition@FilterRules[x, y];
Alternatively, I could have written ReplaceAt
in terms of ReplaceThen
. The below alternative relies upon the fact that RuleDelayed
consumes Unevaluated
from any expression which starts with Unevaluated
:
ClearAll[ReplaceAt]
Options[ReplaceAt] =
Join[Options[Replace], {OperatorHold -> {SequenceHold}}];
SetAttributes[ReplaceAt, SequenceHold]
ReplaceAt[expr_, rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0}, opts : OptionsPattern[]] :=
With[{subExpr =
Extract[Unevaluated@expr, partSpec,
ReplaceThen[rules, levelSpec, Unevaluated, opts,
OperatorHold -> {SequenceHold, HoldFirst}]]},
ReplacePart[Unevaluated@expr, partSpec :> subExpr]]
ReplaceAt[rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0}, opts : OptionsPattern[]
] := With[{replaceThenOp =
ReplaceThen[rules, levelSpec, Unevaluated, opts,
OperatorHold -> {SequenceHold, HoldFirst}]},
Function[expr,
With[{subExpr = Extract[Unevaluated@expr, partSpec, replaceThenOp]},
ReplacePart[Unevaluated@expr, partSpec :> subExpr]],
Evaluate@OptionValue@OperatorHold]]
That concludes the function definitions. Now, let's test the functions against the minimal examples I posted earlier (I've of course done a bit more testing than this, which I won't include, for brevity):
Input:
ReplaceThen[Unevaluated[1 + 1], 1 -> 2, {-1}, Hold]
ReplaceAt[Hold[1 + 1, 1 + 1], 1 -> 2, {2}, {-1}]
Output:
Hold[2 + 2]
Hold[1 + 1, 2 + 2]
The results are what I would expect. That said, perhaps there are some edge-cases that I haven't thought through, or performance/flexibility could be increased. Or perhaps you can think of an alternative way to perform the same tasks, written in WFL, which is simply more elegant.
Thanks in advance for feedback / critiques!
ReplaceThen
is... justReplace
? In other words, "Extract-with-third-argument" singles out a part, then does something to it. Isn't that what a transformationRule
does already (i.e.Replace[ a_ :> f[a] ]
)? Also, you say that you "could not find any function which performs a Replace-like operation ... but only on a portion of an expression specified by an arbitrary part specification". Wouldn't that beMapAt[Replace[...], yourPosition]
? $\endgroup$EvaluateSome
", which works as-posted, but could be both optimized and made more flexible) $\endgroup$