93
$\begingroup$

This question mentions "x := x = trickery".

What does defining a function as f[x_] := f[x] = ... do and what is it good for?

$\endgroup$
2
  • 9
    $\begingroup$ You can also search for "memoization" both here and on SO (StackOverflow) Mathematica tag, to get many examples where this construct is used. $\endgroup$ Commented Mar 6, 2012 at 16:26
  • 6
    $\begingroup$ In short, it means "when x is called, evaluate x = ...". This way not only do you get x back but you also store the value. If you do only "x := ..." then x will remain unassigned. $\endgroup$
    – Timo
    Commented Mar 6, 2012 at 18:51

5 Answers 5

95
$\begingroup$

It is a simple way to implement Memoization. The trick is that if you define a function as

f[x_]:=f[x]=ExpensiveFunctionOf[x]

then when you for the first time call e.g. f[3], it will evaluate as

f[3]=ExpensiveFunctionOf[3]

which will evalulate the expensive function, and assign the result to f[3] (in addition to giving it back, of course). So if e.g. ExpensiveFunctionOf[3] evaluates to 7, it will assign f[3]=7.

The next time you call f[3], it will find that newly created definition of f[3] and use that in preference to the general rule. This means it will return the value 7 without calculating ExpensiveFunctionOf[3] a second time.

$\endgroup$
93
$\begingroup$

Memoization is perhaps the most common application, but it is not the meaning of that construct.

More generally it is a construct for a function that redefines itself. This has many uses beyond memoization. Consider this function:

f[y_] := (f[y] = Sequence[]; y)

It is used to remove duplicates in a list. When the function is first called with a particular argument (expression) it redefines itself, only for that argument, to Sequence[]. The next time it is applied to that expression it therefore resolves to Sequence[] (because the more specific definition has priority) and the expression is effectively removed. (Of course it must be reinitialized between lists unless you want to remove duplicates globally.)

f /@ {3, 5, 2, 3, 2, 4, 3}
{3, 5, 2, 4}

You can also write a function that changes its behavior after the first use.
Note the different pattern name to avoid conflict.

g[x_] := (g[y_] := y - 1; x + 1)

g /@ {1, 2, 3}
{2, 1, 2}

This can even be nested deeper:

g[x_] := (g[y_] := (g[z_] := z + 4; y - 1); x + 1)

g /@ {1, 2, 3}
{2, 1, 7}

Syntax for memoization

When using this construct for memoization it is often nice to use a named pattern for the entire left-hand-side, e.g.:

mem : f[x_] := mem = . . .

This is equivalent to f[x_] := f[x] = . . .. In this simple case it is not an improvement, but when the definition becomes longer it has the advantages of being:

  • more concise
  • less prone to error as there is less code to retype
  • a convenient label for purpose of the construct

If one adopts this convention seeing mem : will immediately let one know this is a memoized function.

As a contrived example:

mem : combinations[set_Integer?Positive, choose_Integer?Positive] := 
  mem = set!/(choose! (set - choose)!)

Memoization is not the only application of a pattern that matches the entire left-hand-side and by using consistent pattern names for each application one can impart valuable metadata about the nature of the code to follow.

Reference:

$\endgroup$
21
  • 2
    $\begingroup$ Very good points. +1 $\endgroup$ Commented Mar 7, 2012 at 6:49
  • $\begingroup$ You can avoid reinitialization if you wrap the application in an Internal`InheritedBlock like this: Internal`InheritedBlock[{f}, f/@ {3,5,2,3,2,4,3}] $\endgroup$
    – celtschk
    Commented Mar 7, 2012 at 11:34
  • $\begingroup$ @celtschk interesting method, although if f were ever used outside InheritedBlock the result could be very frustrating. $\endgroup$
    – Mr.Wizard
    Commented Mar 7, 2012 at 13:58
  • 3
    $\begingroup$ This is basically the function I came up with back in 1999 $\endgroup$
    – Carl Woll
    Commented Mar 7, 2017 at 20:29
  • 1
    $\begingroup$ @CarlWoll For what it's worth I now see Leonid already noted as much in a comment: mathematica.stackexchange.com/questions/2419/… $\endgroup$
    – Mr.Wizard
    Commented Mar 7, 2017 at 20:53
39
$\begingroup$

This is my first reply in this group. So please bear with me if I make any mistake, it would not be intentional, just lack of familiarity with the rules.

Although the replies above mention important aspects, I generally like to view things from alternative perspectives. I'd like to offer a few of those on this question. Understanding is enhanced by viewing the same thing from different angles.

The word "memoization" was already mentioned above by Leonid, you can also think of it as "caching". Consider it as an example of caching data to prevent re-computation. Using the Fibonacci numbers as a simple example, if you have a definition like

fib[n_]:=fib[n]=fib[n-1]+fib[n-2];
fib[0]=1;
fib[1]=1;
fib@5

??fib

then every time you evaluate fib@somethingalreadycomputed M will simply pull the result from memory and not compute it again. This type of construction ensures that you ONLY compute a new result if none is known already for the given argument. In fact, the very fact that the formula is shown at the bottom is exactly how M treats this internally: like a look-up table. You are guaranteed that the order in which ?? (Information) outputs the results is exactly how M looks these things up internally. So the formula is ONLY evaluated if no prior match is found. This is how a typical look-up table works in a software system, this has nothing to do with M. You go through your list sequentially, and at the first match, bail out with the result. That's why it's important to have a look-up table in the proper order, which M ensures us. ?? (Information) outputs exactly the way the look-up table is treated in the kernel.

Compare this with

Clear@fib;
fib[n_]:=fib[n-1]+fib[n-2];
fib[0]=1;
fib[1]=1;
fib@5

??

where the intermediate values are not defined, but have to be computed and re-computed every time you evaluate fib@someInteger. This can be atrociously inefficient for larger n when the function has above-linear complexity.

Another perspective is to liken this to (a particular implementation of) the proxy pattern in object-oriented design. For example, in Java or C# code you frequently find constructs such as

public void displayImage() {
       if (image == null) {
           image = new RealImage(filename);
        } 
        image.displayImage();
    }

If the image doesn't exist, create it, then display it. If it does exist, skip the creation part, and display immediately what we have already in memory. The visitor pattern in OO design also oftentimes works like this. You check for existence, and you base your next decision on the answer to the existence question. This is the most efficient way to ensure that every unique object is only computed once ever.

Another perspective to think of this is as a combination of computation and data. When you compute something with a formula, you are performing a computation. But when you look up something from memory, you are not computing, you are pulling data and return data. This construction is a very convenient way to combine computation and data and let the system decide which one is to be done at execution time. That's actually a very important concept in software design: data vs. computation. We may not think about it this way because it's so easy in M.

Next,

TreeForm@Hold[fib[n_]:=fib[n]=fib[n-1]+fib[n-2]]

tells us that the SetDelayed is evaluated as the outermost function. Thus, the parentheses are

fib[n_]:=(fib[n]=fib[n-1]+fib[n-2]);

Now it's easy to see what happens. Every time you call fib[someInteger], we get the result of an equation (Set). In M the result of an equation (Set) is either the known result (if the evaluated left-hand side is known), or the result of the computation of the right-hand side (if the left-hand side is not known -- yet!). In the latter case it now assigns it to the evaluated left-hand side fib[n] and will stored as such, as downvalues are attached to the left-hand side of their assignments.

$\endgroup$
3
  • 1
    $\begingroup$ Welcome to Mathematica.SE! It's great to see you contribute here $\endgroup$
    – Szabolcs
    Commented Jun 28, 2012 at 19:55
  • $\begingroup$ Don't be shy, this is a fine answer (+1). Hope to see more of you here! $\endgroup$ Commented Jun 28, 2012 at 19:58
  • $\begingroup$ Good one with the QR code... welcome to der Party! $\endgroup$
    – Yves Klett
    Commented Mar 9, 2013 at 9:06
30
$\begingroup$

It's ... oh, why not let the docs speak:

tutorial/FunctionsThatRememberValuesTheyHaveFound

(in Doc center)

Edit

You may also find additional information by searching for "memoization" on this site. This has always been a great trick to avoid having to re-evaluate the result of a computationally intensive function call. In the above link, it is also used to do recursion.

I'll try to explain things without the added complication of recursion: The basic idea is that when you define a function using

f[x_]:= x^2

you are using x as the name of a pattern playing the role of a dummy variable. The := (meaning SetDelayed) has the property of leaving whatever is on the right-hand side unevaluated until it's needed, and this happens when a pattern matching the left side is encountered, say, f[4].

To make Mathematica remember things, you can't assign a value to a pattern, so you would normally say

f[4]=16

Now such definitions are found by Mathematica before it looks for matching pattern definitions such as f[x_]:=x^2 above. So in other words, if I type f[4] after having executed the above two lines, my "function definition" doesn't actually have to be used at all because the system already knows the result for the specific value 4.

The memoization trick now combines the above lines, which would lead to

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

The right-hand side of SetDelayed is now telling us to take whatever was passed in through the dummy variable x and assign this to f[x] using Set (the = sign). The result of that last operation is that a "non-pattern" f[x] with a specific new value of x has been defined for later use, and the value that got assigned to that is also returned as the replacement for the pattern f[x_] (i.e., the function value in the initial function call).

Whenever a new x is passed to f[x], we now get a new "permanent" definition added to the memory so that the function (the pattern) doesn't need to be evaluated again for that x.

It's best to play around with this yourself by defining a function along the lines above, and then periodically checking what Mathematica knows about your function by typing ?f.

$\endgroup$
0
21
$\begingroup$

It also has another practical side. If you have a random function, which

  • you only want to evaluate once, but
  • you don't know where exactly it will be evaluated or
  • you want to declare it before the parameters it depends on are defined, and
  • you want it to be the same after the first evaluation, for any subsequent call,

you can use the memoization trick:

initPopulation := initPopulation = InitializeRandomPopulation[args..];

(* lots of computation *)
(* the above *args* (on which the function depends!) are defined only now *)
(* and finally we got to the part where we compute the initial population *)

pop1 = initPopulation

(* use the very same initial population again and again... *)

pop2 = initPopulation 

...
$\endgroup$

Your Answer

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

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