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$
7
  • 3
    $\begingroup$ Would this do what you want? as = Association["key" -> x]; f3[xx_] := as /. x -> xx $\endgroup$
    – MarcoB
    Apr 24, 2018 at 13:59
  • 1
    $\begingroup$ See this for the difference between definitions for your f1 and f2 functions. $\endgroup$
    – Jason B.
    Apr 24, 2018 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, 2018 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, 2018 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, 2018 at 16:49

3 Answers 3

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$
4
  • 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, 2018 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, 2018 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, 2018 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, 2018 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$
4
  • $\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, 2018 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, 2018 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, 2018 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, 2018 at 16:37

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.