How to avoid nested With[]?
I don't think one can avoid the need for nested With
altogether - I find it a very common case to need declared variables use previously declared variables.
Since I once wrote the function (actually macro) that automates nesting With
, and generates nested With
at run-time, this is a good opportunity to (re)post it as an answer to an exact question that it actually addresses. I will partly borrow the discussion from this answer.
Implementation
Edit Aug.3, 2015 - added RuleDelayed
UpValue, per @Federico's suggestion
Here is the code for it (with added local-variable highlighting):
ClearAll[LetL];
SetAttributes[LetL, HoldAll];
SyntaxInformation[LetL] = {
"ArgumentsPattern" -> {_, _},
"LocalVariables" -> {"Solve", {1, Infinity}}
};
LetL /: (assign : SetDelayed | RuleDelayed)[
lhs_,rhs : HoldPattern[LetL[{__}, _]]
] :=
Block[{With},
Attributes[With] = {HoldAll};
assign[lhs, Evaluate[rhs]]
];
LetL[{}, expr_] := expr;
LetL[{head_}, expr_] := With[{head}, expr];
LetL[{head_, tail__}, expr_] :=
Block[{With}, Attributes[With] = {HoldAll};
With[{head}, Evaluate[LetL[{tail}, expr]]]];
What it does is to first expand into a nested With
, and only then allow the expanded construct to evaluate. It also has a special behavior when used on the r.h.s. of function definitions performed with SetDelayed
.
I find this macro interesting for many reasons, in particular because it uses a number of interesting techniques together to achieve its goals (UpValues
, Block
trick, recursion, Hold
-attributes and other tools of evaluation control, some interesting pattern-matching constructs).
Simple usage
First consider simple use cases such as this:
LetL[{a=1,b=a+1,c=a+b+2},{a,b,c}]
{1,2,5}
We can trace the execution to see how LetL
expands into nested With
:
Trace[LetL[{a=1,b=a+1},{a,b}],_With]
{{{{With[{b=a+1},{a,b}]},With[{a=1},With[{b=a+1},{a,b}]]}, With[{a=1},With[{b=a+1},{a,b}]]}, With[{a=1},With[{b=a+1},{a,b}]],With[{b$=1+1},{1,b$}]}
Definition-time expansion in function's definitions
When LetL
is used to define a function (global rule) via SetDelayed
, it expands not at run-time, but at definition-time, having overloaded SetDelayed
via UpValues
. This is essential to be able to have conditional global rules with variables shared between the body and the condition semantics. For a more detailed discussion of this issue see the linked above answer, here I will just provide an example:
Clear[ff];
ff[x_,y_]:= LetL[{xl=x,yl=y+xl+1},xl^2+yl^2/;(xl+yl<15)];
ff[x_,y_]:=x+y;
We can now check the definitions of ff
:
?ff
Global`ff ff[x_,y_]:=With[{xl=x},With[{yl=y+xl+1},xl^2+yl^2/;xl+yl<15]] ff[x_,y_]:=x+y
Now, here is why it was important to expand at definition time: had LetL
always expanded at run time, and the above two definitions would be considered the same by the system during definition time (variable-binding time), because the conditional form of With
(also that of Module
and Block
) is hard-wired into the system; inside any other head, Condition
has no special meaning to the system. The above-mentioned answer shows what happens with a version of Let
that expands at run time: the second definition simply replaces the first.
Remarks
I believe that LetL
fully implements the semantics of nested With
, including conditional rules using With
. This is so simply because it always fully expands before execution, as if we wrote those nested With
constructs by hand. In this sense, it is closer to true macros, as they are present in e.g. Lisp.
I have used LetL
in a lot of my own applications and it never let me down. From my answers on SE, its most notable presence is in this answer, where it is used a lot and those uses illustrate its utility well.
Introduced in V10.4 or earlier, but after V10.1
This functionality has snuck into With
(ref: Daniel's comment).
Note the use of the braces.
With[{v1 = #}, {v2 = f[v1]}, g[v1, v2]]
(* g[#1, f[#1]] *)
The syntax coloring has not caught up yet:
In V10 --
Needs["GeneralUtilities`"];
?GeneralUtilities`Where
Where[ass1, ass2, ..., expr]
is a version of With that supports multiple sequential assignments.
Needs["GeneralUtilities`"];
Where[v1 = #, v2 = f[v1], g[v1, v2]]
(* g[#1, f[#1]] *)
Where[x = 2, t = x^2, Hold[x + t]]
(* Hold[2 + 4] *)
With
works by performing a substitution operation prior to executing its body, and likely it is only a single pass. So, inter-referencing the variables is not possible. Since With
accepts the use of SetDelayed
(:=
), you might think that that could be used, instead. For example,
With[{v1 = #, v2 := f[v1]}, g[v1, v2]]& @ p
(* g[p, f[v1]] *)
which reveals the other use of With
: localization. The v1
in f[v1]
is not the same as the v1
used by With
, so that method is out, also. The same problem exists for Module
as it uses a similar form of localization.
However, Block
works
Block[{v1 = #, v2 = f[v1]}, g[v1, v2]]& @ p
(* g[p, f[p]] *)
even though the syntax highlighting makes it appear that the v1
inside f
is not localized. But, Block
has the attribute HoldAll
, so v2 = f[v1]
is not executed until v1
has taken on its local definition, and, unlike in With
and Module
, it is not internally treated with Unique[v1]
.