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. 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, so I created a ReplaceThen
function which adds this option.
Additionally, I could not find any function which performs a Replace
-like operation (including arbitrary pattern rules, not just substitution of one part with a verbatim-substituted replacement part), but only on a portion of an expression specified by an arbitrary part specification (not just a level specification). So… I created a ReplaceAt
function which adds this functionality.
That said, 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. For example, I considered an approach based upon using
Cases
and Position
, but decided that the below implementation would be more efficient.
- 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 solved by my functions, 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. Would you use these functions for general-purpose use involving large replacement operations as-written, or might these warrant being written in C? My WFL programming skills are actually significantly weaker than their C equivalents, so it makes little difference to me. My guess is that since
Replace
, ReplacePart
, Extract
, and Dispatch
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. The code is 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 *)
Options(ReplaceThen) = Options(Replace);
Options(ReplaceAt) = Options(Replace);
Next, here are ReplaceThen
and ReplaceAt
:
With({ruleExceptTransform = ruleExceptTransform,
levelSpecTransform = levelSpecTransform},
ReplaceThen(expr_, rules : replaceRulePatt,
levelSpec : levelSpecPattern : {0}, wrap_Symbol : Identity,
opts : OptionsPattern()) := wrap @@ Replace(
ReplaceAt`HoldComplete(expr),
Replace(rules, ruleExceptTransform, {0, 1}),
Replace(levelSpec, levelSpecTransform),
opts
);
ReplaceAt(expr_, rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0}, opts : OptionsPattern()) :=
ReplacePart(
Unevaluated@expr,
Replace(
Replace(
Extract(Unevaluated@expr, partSpec, ReplaceAt`HoldComplete),
Replace(rules, ruleExceptTransform, {0, 1}),
Replace(levelSpec, levelSpecTransform),
opts
),
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 : Identity,
opts : OptionsPattern()) :=
Function(expr, ReplaceThen(expr, rules, levelSpec, wrap, opts),
HoldFirst) /. DownValues@ReplaceThen;
(* Remember Unevaluated if not inlining with DownValues *)
ReplaceAt(rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0}, opts : OptionsPattern()) :=
Function(expr,
ReplaceAt(expr, rules, partSpec, levelSpec, opts)) /.
DownValues@ReplaceAt;
Alternatively, I could have written ReplaceAt
in terms of ReplaceThen
, but I think that my formal proposal for these functions would be as-written above. The below alternative relies upon the fact that RuleDelayed
consumes Unevaluated
from any expression which starts with Unevaluated
:
ReplaceAt(expr_, rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0}, opts : OptionsPattern()) :=
With({subExpr =
Extract(Unevaluated@expr, partSpec,
ReplaceThen(rules, levelSpec, Unevaluated, opts))},
ReplacePart(expr, partSpec :> subExpr))
ReplaceAt(rules : replaceRulePatt, partSpec : partSpecPatt,
levelSpec : levelSpecPattern : {0}, opts : OptionsPattern()
) := With({replaceThenOp =
ReplaceThen(rules, levelSpec, Unevaluated, opts)},
Function(expr,
With({subExpr = Extract(Unevaluated@expr, partSpec, replaceThenOp)},
ReplacePart(expr, partSpec :> subExpr))))
That concludes the function definitions. Now, let’s define a test expression to run through the two functions and their Operator-form versions:
ClearAll(F, G)
testExpr = Hold(
1 + 1, F(Plus(a, b), Plus(c, d), Times(e, Plus(f, g))), 2 + 2, h + i
);
Then, let’s test ReplaceThen
:
ReplaceThen(testExpr, F :> G, {2}, Heads -> True)
ReplaceThen(Unevaluated(3 + 3), Plus :> Times, {1}, Hold, Heads -> True)
ReplaceThen(Plus :> Times, {1}, Hold, Heads -> True)
%@Unevaluated(3 + 3)
The output is what I would expect:
Hold(1 + 1, G(a + b, c + d, e * (f + g)), 2 + 2, h + i)
Hold(3 * 3)
Function(expr$,
Hold @@ Replace(ReplaceAt`HoldComplete(expr$),
Replace(Plus :> Times, Dispatch(Length : 1), {0, 1}),
Replace({1}, Dispatch(Length : 3)), Heads -> True), HoldFirst)
Hold(3 * 3)
Now, let’s test ReplaceAt
:
ReplaceAt(testExpr, Plus :> Times, {2}, {0, Infinity}, Heads -> True)
ReplaceAt(testExpr, F :> G, {2}, {1}, Heads -> True)
ReplaceAt(testExpr, x_ :> x^2, {2}, {-1})
ReplaceAt(testExpr, x_ :> x^2, {2}, {-Infinity, Infinity})
ReplaceAt(x_ :> x^2, {2}, {-Infinity, Infinity})
%@testExpr
Again, the results match what I would expect:
Hold(1+1,F(a * b,c * d,e * (f * g)),2+2,h+i)
Hold(1+1,G(a+b,c+d,e * (f+g)),2+2,h+i)
Hold(1+1,F(a^2+b^2,c^2+d^2,e^2 * (f^2+g^2)),2+2,h+i)
Hold(1+1,F((a^2+b^2)^2,(c^2+d^2)^2,(e^2 * (f^2+g^2)^2)^2)^2,2+2,h+i)
Function(expr$,
ReplacePart(Unevaluated(expr$),
Replace(Replace(
Extract(Unevaluated(expr$), {2}, ReplaceAt`HoldComplete),
Replace(x_ :> x^2, Dispatch(Length : 1), {0, 1}),
Replace({-(Infinity), (Infinity)}, Dispatch(Length : 3))),
ReplaceAt`HoldComplete(x$_) :> {2} :> x$)))
Hold(1+1,F((a^2+b^2)^2,(c^2+d^2)^2,(e^2 * (f^2+g^2)^2)^2)^2,2+2,h+i)
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!
EDIT: I’ll start by critiquing myself — I noticed that I’m inconsistent in the Operator forms of ReplaceThen
and ReplaceAt
… ReplaceThen
uses HoldFirst
, while ReplaceAt
does not. On the one hand, I want to be able to use the Operator forms of these functions as the 3rd argument to Extract
, receive an unevaluated expression, and pass-on that expression, still Unevaluated
, to downstream Replace
/ReplacePart
functions. On the other hand, if the user provides a variable to the Operator forms of my functions (e.g. myReplaceAtOperator(testExpr)
), I want to support that too. But if I make the Operator forms include Function(..., HoldFirst)
and pass-on Unevaluated(expr)
to downstream functions, then Replace
/ReplacePart
will just receive a Symbol
, not the expression it contains. Still thinking this part through… I may just make an OptionValue
so that you can choose when you want the Operator to have HoldFirst
and when you do not…