11
$\begingroup$

I have a function-defining function

SetAttributes[DefFn, HoldAll];

DefFn[f_[args___], body_] :=
 f[args] := body;

I am trying to modify this function to record the function name and the values of its arguments on a stack for error-reporting purposes. I have a function WithStackFrame which adds this information to a list, and I am inserting a call to this function into each definition:

DefFn[f_[args___], body_] :=
 f[args] := WithStackFrame[f,body];

This allows me to give a stack backtrace when reporting errors. This works. The problem comes when I try to store also the values of the function arguments. A naive

DefFn[f_[args___], body_] :=
 f[args] := WithStackFrame[{f,{args}},body];

does not work because args is actually the sequence of patterns. The closest I have got is

DefFn[f_[args___], body_] :=
 Module[{argValueExprs,x},
    argValueExprs = {args} /. x_Pattern :> x[[1]];
    f[args] := WithStackFrame[{f,argValueExprs},body]];

For the purpose of testing, you can use

WithStackFrame[sf_, expr_] := Print[sf];

However, running this gives

DefFn[f[x_], x^2];

f[3]  
(*
{f,{x}}
*)

when I really want {f,{3}}. For some reason, the x is not being evaluated in a scope in which the function arguments are visible.

$\endgroup$
4
  • $\begingroup$ You might want to add a note that WithStackFrame needs attribute HoldAll for the test case to work as stated. $\endgroup$ Feb 23, 2012 at 16:28
  • $\begingroup$ @Albert I'd say, HoldFirst rather than HoldAll, to allow the body to evaluate. $\endgroup$ Feb 23, 2012 at 16:36
  • $\begingroup$ @Leonid: actually now that I think about it even HoldFirst seems not really necessary, it was my mistake (I had a definition for x hanging around) that caused it was needed in the first place. Once the function does what it is supposed, the actual arguments will be used so the hold attribute should usually not be necessary at all, so it might be best to ignore my comment... $\endgroup$ Feb 23, 2012 at 17:22
  • $\begingroup$ @Albert Yes, but the arguments passed to the first argument of WithStackFrame, may be expressions which may evaluate (the function being executed may be HoldAll itself), so in that case HoldFirst for WithStackFrame would prevent that. $\endgroup$ Feb 23, 2012 at 18:12

3 Answers 3

11
$\begingroup$

You could name the patterns

DefFn[f_[args___], body_] := 
  f[s : PatternSequence[args]] := WithStackFrame[{f, {s}}, body];
$\endgroup$
15
  • $\begingroup$ This is pretty cool. I use this myself often, but for this question, this did not cross my mind - too bad. Big +1. $\endgroup$ Feb 23, 2012 at 16:26
  • $\begingroup$ @LeonidShifrin thanks. In an unrelated matter, I just found some behaviour that I found interesting and commented in the main chat but they had their minds somewhere else. I now discovered it's based on the Update[] issue but I would like to know what you think, if it would be useful, if you have a minute :) $\endgroup$
    – Rojo
    Feb 23, 2012 at 16:29
  • $\begingroup$ I like that for it's simplicity $\endgroup$ Feb 23, 2012 at 16:29
  • $\begingroup$ @Rojo You are a dangerous person :). You may find my answer in this thread relevant to your suggestion - I used there the one-step evaluation method similarly to your suggestion. $\endgroup$ Feb 23, 2012 at 16:34
  • $\begingroup$ Thanks @LeonidShifrin... For the link and comment, not so much for the dangerous... A little limitation of using DownValues however is that they don't work well with Flat (which can't get any more complex). An advantage is that it isn't based on a bug, hehe $\endgroup$
    – Rojo
    Feb 23, 2012 at 16:49
5
$\begingroup$

You need a parser for the argument patterns. I wrote a simplistic one for this answer. I will reproduce it here to keep things self-contained:

splitHeldSequence[Hold[seq___], f_: Hold] := List @@ Map[f, Hold[seq]];

getFunArguments[Verbatim[HoldPattern][Verbatim[Condition][f_[args___], test_]]] := 
     getFunArguments[HoldPattern[f[args]]];

getFunArguments[Verbatim[HoldPattern][f_[args___]]] := 
     FunArguments[FName[f], FArgs @@ splitHeldSequence[Hold[args]]];

(*This is a simplistic "parser".It may miss some less trivial cases*)

getArgumentNames[args__FArgs] := 
   args //. {
     Verbatim[Pattern][tag_, ___] :> tag, 
     Verbatim[Condition][z_, _] :> z, 
     Verbatim[PatternTest][z_, _] :> z
   };

With this, your code might be:

ClearAll[DefFn];
SetAttributes[DefFn, HoldAll];
DefFn[f_[args___], body_] :=
  With[{funargs = getFunArguments[HoldPattern[f[args]]]},
     With[{fargs = Join @@ getArgumentNames[funargs[[2]]]},
        SetDelayed @@ Hold[f[args], WithStackFrame[{f, fargs}, body]]]];

Using it:

DefFn[f[x_], x^2];

?f
Global`f
f[x_]:=WithStackFrame[{f,Hold[x]},x^2]

and thus:

f[3]

(*
 ==> {f,Hold[3]}
*)

EDIT

Here is a somewhat more general and more robust argument parser:

ClearAll[parse];
SetAttributes[parse, HoldAll];
parse[(Condition | PatternTest | Optional)[arg_, _]] := parse[arg];
parse[(HoldPattern | Optional)[arg_]] := parse[arg];
parse[Verbatim[Pattern][sym_, _]] := Hold[sym];
parse[Verbatim[Repeated][p_, ___]] := parse[p];
parse[(Blank | BlankSequence | BlankNullSequence)[___]] := Hold["NotAPatternVar"];
parse[(Longest | Shortest)[arg_, ___]] := parse[arg];
parse[Verbatim[PatternSequence][args___]] := parse[args];
parse[a_ /; AtomQ[Unevaluated[a]]] := Hold["NotAPatternVar"];
parse[args___] := Join @@ Map[parse, Unevaluated /@ Unevaluated[{args}]];
parse[f_[args___]] := {Hold[f], parse[args]};

With its help, the function would be written as:

ClearAll[DefFn];
SetAttributes[DefFn, HoldAll];
DefFn[f_[args___], body_] :=
  With[{fargs = parse[f[args]]},
     SetDelayed @@ Hold[f[args], WithStackFrame[fargs, body]]];

Some examples to try:

ClearAll[f, ff];
DefFn[f[x_], x^2];
DefFn[
  ff[x_,PatternSequence[Shortest[y__], p__] /; {Length[{p}] < Length[{y}]},z_, (q_) ..] /; 
         x < z, 
  {x, y, p, z, q}]

Note that the trick SetDelayed@@Hold[lhs,rhs] is needed to fool the variable renaming mechanism of enclosing With. This is explained in more detail here.

$\endgroup$
7
  • $\begingroup$ Faster and more elaborate than mine -- of course :-) $\endgroup$ Feb 23, 2012 at 16:27
  • $\begingroup$ @Albert You should remove this "of course" - I was faster by pure luck, and your solution is simpler. $\endgroup$ Feb 23, 2012 at 16:28
  • $\begingroup$ You should also include Optional in your parser as it wraps Pattern like PatternTest does, e.g. FullForm[HoldPattern[a_:0]] returns HoldPattern[Optional[Pattern[a, Blank[]],0]. $\endgroup$
    – rcollyer
    Feb 23, 2012 at 16:35
  • $\begingroup$ @Leonid: I just deleted mine, since it just is a simpler version of yours. You might want to add an explanation that SetDelayed@@Hold[...] is just a trick to avoid the localization of SetDelayed that was the root of the problem that Ian has seen. $\endgroup$ Feb 23, 2012 at 16:37
  • $\begingroup$ @rcollyer Yes, you are right. Actually, the parser itself is not written well, because it should be true recursive decsent, while I took a short-cut and substituted it with repeated rule application through ReplaceRepeated. I will take some time and rewrite it, then update the answer - and incorporate your suggestion as well. $\endgroup$ Feb 23, 2012 at 16:39
4
$\begingroup$

I would do this:

DefFn[f_[args___], body_] := 
  lhs : f[args] := WithStackFrame[lhs, body];

Then make WithStackFrame HoldFirst and do de-structuring there. For example:

SetAttributes[WithStackFrame, HoldFirst]

WithStackFrame[f_[args___], expr_] := Print[{f, {args}}];

If for some reason this were unacceptable I would do:

DefFn[f_[args___], body_] := 
  lhs : f[args] := WithStackFrame[{f, List @@ Unevaluated[lhs]}, body];
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.