Defining functions vs using replacement rules

To my knowledge, there are (at least) two ways in which we use functions. The first one is by defining a function

In[1]:= f[x_] := x^2

In[2]:= f[2]

Out[2]= 4


But we can also just write

In[1]:= g = x^2

Out[1]= x^2

In[2]:= g /. x -> 2

Out[2]= 4


These approaches can be generalized to several variables by using f[x1_,x2_..] and g/.{x1->y, x2->y2....}. Is one of these approaches better than the other? Are there situations in which using one is more practical, faster or represents any advantage? Do the previous answers change when we have a lot of functions and a lot of variables?

• Note that if x was not assigned a value then you can also use f[x_]=x^2. Check the syntax coloring to see if the variable has been assigned a value (for example black instead of green but that depends on your settings). f[x_] has the advantage that the right hand side is computed right away if it involves calculations whereas with := it would recompute the calculations each time. := can also be desirable if we do not want the computation to happen right away. For example when the right hand side involves NIntegrate with symbollic parameters. Dec 16, 2022 at 4:18
• := can also lead to issues when mixed with functions that use the type of scoping that Block uses and can be hard to debug for example mathematica.stackexchange.com/a/273480/86543. Dec 16, 2022 at 4:21
• I think f[x_]:=x^2 is on the save side. If you use f=x^2 and x gets a value or changes its value later, the result of f will also change. Dec 16, 2022 at 8:42

The form like g = x^2 generates OwnValues for g. The form like f[x_] := x^2, creates DownValues for the symbol f. There are other definitions that can get attached to symbols, like SubValues and UpValues. These definitions are saved in a special form, and they are "consulted" in a specific order when expressions are being evaluated.

What is probably the most germane to your question is that DownValues carefully handle the named patterns. So, in f[x_] := x^2, we can think of x as a named argument, and so we get x^2 evaluated with whatever was passed in each time we invoke f. In other words, if I were to evaluate x=5, then g would evaluate to 25 every time it was invoked (until x is redefined or g is redefined). Furthermore g /. x -> 2 also evaluates to 25. The first step is to evaluate g, 25 /. x->2, then we evaluate x, 25 /. 5->2, then the ReplaceAll, 25 (since nothing matches the replacement rule). But on the other hand f[7] will produce 49--it doesn't get handcuffed to the fact that we evaluated x=5.

[NOTE: Set versus SetDelayed might be confusing here, but it's not really important. The above is true for both f[x_]:= or f[x_]=, assuming x=5 is evaluated afterward. If x had previously been given a value, then the f[x_]:= form would be the only one that avoids the name collision.]

Are there situations in which using one is more practical, faster or represents any advantage? Do the previous answers change when we have a lot of functions and a lot of variables?

is emphatically "yes". But what choice is best for each situation depends on the situation. It's best to understand Mathematica's evaluation procedure and then match that to your semantics. If I were forced to give a simplistic answer, then DownValues is usually the closest match for function definition.

As far as concerns differences, see these two (badly) defined factorial computations

facIter[n_] := n*facIter[n - 1]
facIter[5]
(* Hold[5 facIter[5 - 1]] *)


and because stopping condition is absent it issues Message \$Recursion limit.

On the other side similar bad approach using rules yields

    rules = {fac[n_] :> n*fac[n - 1]};
fac[5] //. rules

(* 0 *)


and no warning.