2
$\begingroup$

I have an Association[] which is the result of an intensive computation. Its values include the variable x and I would like to turn the entire Association[] into a set of Functions.

I could use a Delayed Definition (Reference) using the := function (SetDelayed). However, this would redo the initial computation every time, which is inefficient. I prefer using an Immediate Definition using = for the Function. However, that does not seem to work.

The following Mathematica code illustrates the problem:

f1[x_]=Association["linear"->x, "square"->x^2];
f1[3]["square"] (* x^2 *)
f2[x_]:=Association["linear"->x, "square"->x^2]
f2[3]["square"] (* 9 *)

Function f1 does not produce the desired result, whereas f2 does. How can I obtain the desired result? If possible, please explain the underlying theory, such that I will be able to tackle such problems myself in the future.

$\endgroup$
  • 3
    $\begingroup$ Would this do what you want? as = Association["key" -> x]; f3[xx_] := as /. x -> xx $\endgroup$ – MarcoB Apr 24 '18 at 13:59
  • 1
    $\begingroup$ See this for the difference between definitions for your f1 and f2 functions. $\endgroup$ – Jason B. Apr 24 '18 at 14:01
  • $\begingroup$ @MarcoB Thanks, indeed it works. However, it feels a bit like a cheat. And I still don't understand why it does not work out-of-the-box. $\endgroup$ – LBogaardt Apr 24 '18 at 14:34
  • $\begingroup$ @JasonB. Thanks for pointing to the nomenclature of Set and SetDelayed. However, I still don't get why Association[] needs a SetDelayed instead of a Set. $\endgroup$ – LBogaardt Apr 24 '18 at 14:36
  • 1
    $\begingroup$ As to why it does not work "out-of-the-box", it has to do with the ambiguity between Association as constructor function and Association as head of an atomic association object. See (148074) for more discussion on this point. The solution suggested by @MarcoB addresses this ambiguity directly. $\endgroup$ – WReach Apr 25 '18 at 16:49
1
$\begingroup$

I think the "why" has already been covered quite well by the previous answers, but the "how" could probably be expanded on a bit. If you want to be able to use syntax like f1[3]["square"] you could use SubValues instead:

f3[x_]["linear"] = x;
f3[x_]["square"] = x^2;

In[13]:= f3[2]["square"]

Out[13]= 4

Or just do f3[x_, "linear"] = x;, which is probably even easier. If you insist on having everything inside of an Association, I personally think the best way is to build an Association of Functions. I'm going to assume you start with some big expression with xes in it, so you need to convert that to a function of x:

In[21]:= f4 = Function[{x}, #] & /@ <|"linear" -> x, "square" -> x^2|>

Out[21]= <|"linear" -> Function[{x}, x], "square" -> Function[{x}, x^2]|>


In[22]:= f4["square"][3]

Out[22]= 9

As you can see, you now need to first specify the key before the value. The added bonus here is that (for purely numeric functions) you can substitute Function with Compile to get an extra boost in evaluation speed.

I'd also like to add that there's a good reason why you wouldn't want to do this in the way the OP asked for. If the syntax f1[3]["linear"] actually worked, it would still compute both 3 and 3^2 (and everything else you might have stored in the association) even though you only need the first key. Sure, the squaring might not be super intensive, but it's still extra work you're throwing away right afterwards. Both of the methods I proposed here avoid this issue.

$\endgroup$
5
$\begingroup$

The problem you are encountering is related to the fact that Association objects are atomic:

Association["key"->x] //AtomQ

True

Since they are atomic, their contents can not be bound to a pattern variable. For a different example, consider:

(f[x_] = Graph[{1->2}, EdgeWeight->{x}, EdgeLabels->"EdgeWeight"]) //InputForm

Graph[{1, 2}, {DirectedEdge[1, 2]}, {EdgeLabels -> {"EdgeWeight"}, EdgeWeight -> {x}}]

f[1] //InputForm

Graph[{1, 2}, {DirectedEdge[1, 2]}, {EdgeLabels -> {"EdgeWeight"}, EdgeWeight -> {x}}]

The SetDelayed variant works because the Association has not evaluated, so it is not yet atomic.

$\endgroup$
  • 2
    $\begingroup$ So a this point (with sparse arrays, associations, and datasets all being atomic), the notion of atomicity has shifted quite a bit from the early intuition for numbers, strings, and symbols. Is that fair? $\endgroup$ – Alan Apr 24 '18 at 17:40
  • $\begingroup$ Thanks for the explanation. Does this mean the only remaining option is the one by MarcoB, where the Association and its content are evaluated first and subsequently altered used a Rule? Quoting MarcoB: as = Association["key" -> x]; f3[xx] := as /. x -> xx_ $\endgroup$ – LBogaardt Apr 26 '18 at 8:11
  • $\begingroup$ @LBogaardt Can you provide an example where using SetDelayed causes an issue for you? There may be a better approach then switching to Set $\endgroup$ – Carl Woll Apr 26 '18 at 14:28
  • $\begingroup$ @CarlWoll My specific use-case is not easy to explain briefly. Just imagine there are various functions which solve various differential equations. The solutions to each of these are stored in my Association under some key. Now, I would like to use them as a regular function (e.g. Plot), by choosing a key and then filling in the argument. But I do not want to solve the differential equation every time. $\endgroup$ – LBogaardt Apr 27 '18 at 15:32
1
$\begingroup$

Pattern-based function definition works by specifying rewrite rules. These rules can be seen with the DownValues command. When you use Set (=) the right-hand side is immediately evaluated. In this case, the difference between your two functions is the difference in whether or not the right-hand side was evaluated in producing the rewrite rule. So here is an equivalent way to define these two functions:

ClearAll[f1,f2,x]
DownValues[f1]= List[RuleDelayed[HoldPattern[f1[Pattern[x,Blank[]]]],Evaluate@Association[Rule["key",x]]]]
DownValues[f2]= List[RuleDelayed[HoldPattern[f2[Pattern[x,Blank[]]]],Association[Rule["key",x]]]]
f1[123]  (* <|"key" -> x|> *)
f2[123]  (* <|"key" -> 123|> *)

As you can see, the only difference is whether or not we forced evaluation of the association.

$\endgroup$
  • $\begingroup$ True, but this explains the difference between Set and SetDelayed. It does not address why Associations do not allow for a pattern variable (the Atomic reason by Carl Woll). In fact, change Association to List in your example, and the problem vanishes. $\endgroup$ – LBogaardt Apr 26 '18 at 8:08
  • $\begingroup$ @LBogaardt Yes, atomicity is important. So is the order of evaluation. See the code in my answer: the two down values are identical except for the Evaluate. I made no use of SetDelayed. $\endgroup$ – Alan Apr 26 '18 at 20:16
  • $\begingroup$ I think the effect is the same though. It would re-evaluate the Association every time. In my case, that's the computational bottleneck which I am trying to avoid. The answer by MarcoB solves this, though I am not 100% satisfied with the method. $\endgroup$ – LBogaardt Apr 27 '18 at 15:27
  • $\begingroup$ @LBogaardt Can you explain your dissatisfaction? If it is the reference to a global, you could close over that: With[{as = Association["linear" -> x, "square" -> x^2]}, f[xx_] := (as /. {x -> xx}) ]. However, I would probably g[xx_] := Map[With[{x = xx}, #] &, Association["linear" -> x, "square" -> x^2]] $\endgroup$ – Alan Apr 27 '18 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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