3
$\begingroup$

Post-answer note: this question is (understandably) marked as a duplicate, and indeed has a similar solution, but is subtly different in focus: the linked question is about returning a function call unevaluated, but this is about being able to produce side-effects while doing so, like error messages. The relevant point in @Sjoerd Smit's answer to this question (which isn't so explicit in the other) is that the test in any Condition (/;), even one with an unreachable result, is always evaluated (as part of trying that definition) and so can be used for these side effects—which is pretty nifty!

Consider the (bad) input NSolve[1,1,1]. This yields an error message, and returns NSolve[1,1,1]. This return value seems to be literally the expression NSolve[1,1,1]: NSolve[1,1,1] // FullForm is again NSolve[1,1,1], and it seems usable as an inert expression internally (e.g. Nsolve[1,1,1] /. {1 -> 3} sends two error messages, one each time it attempts evaluation, and ultimately returns NSolve[3,3,3]).

I want to make a function that behaves like this, and I'm curious how Mathematica manages to do this without hitting the recursion limit. Consider a function which throws an error if its argument is 0 and otherwise returns 1.

f::err = "Error!";
f[a_] := If[a == 0, Message[f::err]; f[a], 1]

f[0] obviously goes into an infinite loop of definition application. The attempts

f[a_] := If[a == 0, Message[f::err]; Unevaluated[f[a]], 1]

f[a_] := If[a == 0, Message[f::err]; Return[Unevaluated[f[a]]], 1]

f[a_] := If[a == 0, Message[f::err]; Defer[f[a]], 1]

f[a_] := If[a == 0, Message[f::err]; HoldForm[f[a]], 1]

are all inequivalent to whatever Mathematica does (as one can see upon applying FullForm if necessary).

So what's Mathematica doing, or what's an equivalent implementation? (Am I missing something obvious?) Does Mathematica somehow retain a record of whether it has already tried to evaluate a certain expression during a given evaluation process somehow? Or is there a better "don't evaluate further" head than Unevaluated or Defer out there somewhere?

$\endgroup$
6
  • 2
    $\begingroup$ It does keep track of some things such as whether an expression has been evaluated (I don't know the details, but I seen others mention them here), but I think the basic idea is explained in Evaluation of Expressions: "The Wolfram Language follows the principle of applying definitions until the result it gets no longer changes" $\endgroup$
    – Michael E2
    Jun 14, 2021 at 0:43
  • $\begingroup$ Related: mathematica.stackexchange.com/q/128417/4999 $\endgroup$
    – Michael E2
    Jun 14, 2021 at 0:55
  • 1
    $\begingroup$ For fun, here's a quiz: (A) f[1] := f[1]; g[1] := g[1] + 0; and what happens when we execute f[1] and g[1]? (B) What happens in Block[{x}, NSolve[x, 1, 1]] and Block[{}, NSolve[x, 1, 1]]? (C) What's the difference between NSolve[1, 1, 1] /. 1 -> 1 and NSolve[1, 1, 1] /. 1 -> 3? $\endgroup$
    – Michael E2
    Jun 14, 2021 at 1:00
  • $\begingroup$ @MichaelE2 yes, thanks, it was indeed supposed to be NSolve[1, 1, 1] /. 1 -> 3! $\endgroup$
    – thorimur
    Jun 14, 2021 at 18:12
  • $\begingroup$ @MichaelE2 those are great examples, thanks for them! my answers, in case they're useful to anyone, or in case I have any misunderstandings to be cleared up in them: (A) from looking at TracePrint, it seems that the application of f's definition brings us back to the "top level" at which mathematica can tell that the expression is no longer changing, but the evaluation of g[1] in g[1] + 0 occurs within the evaluation of Plus, and so is not compared to the original g[1]. $\endgroup$
    – thorimur
    Jun 14, 2021 at 18:32

1 Answer 1

5
$\begingroup$

It's pretty simple: you just make sure that the function pattern never matches illegal arguments. For example:

ClearAll[f]
f[a_ /; a != 0] := 1;
f[0]
f[2]

f[0]

1

Sometimes you also want to do something (like issue a message) before returning the expression unevaluated. You can do this by adding to following definition to f (don't clear the previous definition):

f::err = "Bad argument `1`!";
f[arg_] /; (Message[f::err, arg]; False) := "This will never be reached";

f[0]

During evaluation of In[17]:= f::err: Bad argument 0!

f[0]

The message will be generated while checking the condition on f, but the condition will always evaluate to False, so the r.h.s. will never be reached.

Edit

Like Lukas commented, you can do even more sophisticated pattern checks with Condition on the r.h.s. (see documentation). For example:

f[arg_] := Module[{var = preProcess[arg]},
    Condition[
        bodyFunction[var]
        ,
        If[ !FailureQ[var]
            ,
            True
            ,
            Message[f::err, arg];
            False
        ]
    ]
]

In this example, it's assumed that preProcess returns $Failed or Failure[...] if the argument is bad.

$\endgroup$
2
  • 1
    $\begingroup$ It might be nice to add an example showing how to use conditions inside Module to allow for more complex conditions $\endgroup$
    – Lukas Lang
    Jun 14, 2021 at 10:55
  • $\begingroup$ Great, thanks! I was having trouble finding the definition that would get applied but not re-applied infinitely! an unreached condition is exactly it. $\endgroup$
    – thorimur
    Jun 14, 2021 at 18:07

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