5
$\begingroup$

Ok, I know the simple answer is to set some form of Hold attribute to the function but bear with me for a bit while I explain my motivation and why that is not quite what I want.


I have a collection of data that is naturally grouped together and some functions that operate on that data. To me, the obvious way to represent this is a struct-like data-type and an Association seemed perfect for the job. There are a couple of questions and answers describing this approach on this site.

Great, now I have an association as follows: (Just an example)

<|
  "Atom Names" -> {"N","C","O","C","H","H"},
  "Atom Nr"    -> {56,23,117,81,211,5},
  "Resname"    -> {ALA,ALA,TYR,LEU,GLY,GLY},
  "Bias Value" -> {1,5,1,5,1,1}
  "getRandomAtom" :> RandomChoice[{56,23,117,81,211,5}],
  "atomExists" :> (MemberQ[{"N","C","O","C","H","H"},#]&)
|>

I have some other functions that operate on this collection of data.

f1[a_Association, args__] := a["getRandomAtom"] + Total[a["Atom Nr"]]
f2[a_Association, atom_String] := If[a[atomExists][atom], a["Bias Value"] + 1]
(*etc*)

Ideally these function would not just accept any Association but only the 'correct' type of Association. I can ensure this in a couple of ways. Eg. create a helper function and use Condition or PatternTest to check if the Association has all the correct keys or much more simply, I can just wrap the entire Association with a inert head.

Protect[atomData]
atomData[<|
  "Atom Names" -> {"N","C","O","C","H","H"},
  "Atom Nr"    -> {56,23,117,81,211,5},
  "Resname"    -> {ALA,ALA,TYR,LEU,GLY,GLY},
  "Bias Value" -> {1,5,1,5,1,1}
  "getRandomAtom" :> RandomChoice[{56,23,117,81,211,5}],
  "atomExists" :> (MemberQ[{"N","C","O","C","H","H"},#]&)
|>]

And in my functions, I can just check the heads.

f3[a_atomData,args__] := "Do Stuff"

But I'd like to retain the functionality of Association transparently. We can somewhat achieve this through the use of upvalues.

atomData[a_Association][key_] := a[key];
atomData /: h_[atomData[a_Association],args___] := h[a,args] 
(*And some additional stuff*)
atomData /: ToString[atomData[a_Association]] := a["Atom Names"]
atomData[a_Association][] := a

As this question notes, this will not work for all functions (eg. Lookup) as they have the HoldAllComplete attribute. (Coincidentally, my motivation is almost exactly the same as the OP of that question)


Here comes the problem. Since I defined these upvalues, my functions which check for the Head atomData won't work anymore.

f3[myatomdata,3,4] (* myatomdata has head atomData *)

The upvalues associated with atomData will be applied first and this will result in f3[<|...(*underlying association*)...|>,3,4], which will not be evaluated as the first argument nolonger has the Head atomData.

SetAttributes[f3,HoldFirst]

won't help either, as in the function call f3[myatomdata,3,4], the evaluator will leave myatomdata alone, which means that it will have Head Symbol, and once again f3 will not be evaluated.


It seems that I have defeated my entire purpose by setting these upvalues. Is there a better way to do what I want?

I can think of 2 ways, both of which seem quite ugly.

  1. Modify the upvalue definition to exclude certain functions. Something like

     atomData /: 
     (h : Except[f1|f2|f3])[atomData[a_Association], args___] := h[a, args] 
    

This seems particularly inelegant, as I'd have to modify this every time I add another function that will use atomData.

  1. Do the head checking yourself.

     SetAttributes[f3,HoldFirst]
     atomData /: (h : Except[Head])[atomData[a_Association], args___] := h[a, args]
     f3[a_ /; Head[a]===atomData] := "Do Stuff"
    

The ideal solution would be a way to prevent just the upvalue from evaluating and leaving all others (OwnValues,DownValues etc.) alone.


PS. I'm also open to the idea that this whole approach is rubbish if someone can suggest a better way. I come from a background of C++, Java, and Python; Thinking of everything in terms of objects has been ingrained in me. Apologies for the long-winded explanation.

$\endgroup$
4
$\begingroup$

You might consider putting the "type" label inside an Association itself. This will complicate key addressing but simplify other handling.

asc = <|"atomData" ->
    <|"Atom Names" -> {"N", "C", "O", "C", "H", "H"}, 
     "Atom Nr" -> {56, 23, 117, 81, 211, 5}, 
     "Resname" -> {ALA, ALA, TYR, LEU, GLY, GLY}, 
     "Bias Value" -> {1, 5, 1, 5, 1, 1}, 
     "getRandomAtom" :> RandomChoice[{56, 23, 117, 81, 211, 5}], 
     "atomExists" :> (MemberQ[{"N", "C", "O", "C", "H", "H"}, #] &)|>|>;

aDtest[a_Association /; Keys[a] === {"atomData"}] := True;

f1[a_?aDtest, args__] := a[[1]]["getRandomAtom"] + Total[a[[1]]["Atom Nr"]]

f1[asc, 2]
549

Using a[[1]] each time is only one way to approach this; others include:

f1[aa_?aDtest, args__] := 
  With[{a = aa[[1]]}, a["getRandomAtom"] + Total[a["Atom Nr"]]]

Or:

f1[a_?aDtest, args__] := f1core[a[[1]], args]

f1core[a_, args__] := a["getRandomAtom"] + Total[a["Atom Nr"]]

For Mathematica 10.4 or later

The method above can be improved for recent versions of Mathematica as follows:

ClearAll[f1]

p1 = <|"atomData" -> a_|>;

f1[p1, args__] := a["getRandomAtom"] + Total[a["Atom Nr"]]

f1[asc, 2]
549

Reference:

$\endgroup$
  • $\begingroup$ Thank you for the answer. While this does simply many things with regards to handling (I don't have to mess with upvalues anymore), I don't think it is particularly better than my 2nd approach. The more complicated key addressing will produce more clutter or clutter scoping blocks everywhere than just creating the upvalue definitions once off. Plus when I have many of these atomData 'types' doing With[{a1 = aa1[[1]], a2 = aa2[[1]] ...] gets cumbersome. Any thoughts on the PS. part of my question? - if I should continue with the OO paradigm $\endgroup$ – void life Jan 8 at 1:14
  • $\begingroup$ @void If you are using Mathematica 10.4 or later please see my update and tell me if it is more to your liking. $\endgroup$ – Mr.Wizard Jan 8 at 6:23
  • $\begingroup$ That's neat, it seems like the most elegant solution right now. I'm using Mathematica 11.3 so I can take advantage of it. $\endgroup$ – void life Jan 8 at 15:14
3
$\begingroup$

You should definitely go the route of using an inert wrapper for the association, atomData[Association[...]].

You can make a general subvalue like what you have,

atomData[a_Association][key_] := a[key];

and you can make more specific definitions like

atomData[a_Association]["getRandomAtom"] := 
       RandomChoice[a["Atom Names"]

and

atomData[a_Association]["atomExists", s_] := MemberQ[a["Atom Names"], s]

so that you don't need to waste space in your atomData for computed properties. This definition however,

atomData /: h_[atomData[a_Association],args___] := h[a,args]

is, as you say, pure rubbish suboptimal. This upvalue is so general that it renders your object useless for any other purpose. Just try {atomData[<|a->b|>],2}.

I use UpValues so seldom, that I tend to see it as code smell, an indication you should rethink things (except Nothing, I use Nothing in a lot of cases). In the above, the definition should be on h and not on atomData.

In your workarounds, you try to implement a list of special heads that shouldn't trigger an upvalue on atomData. Instead, it should be in the definition of the other functions to access the data in atomData:

f1[a:atomData[assoc_Association], args_] := f[ assoc, args]
f1[a_Association, args_] := ....

Write a definition like the above for every function you want to just work with the raw data directly. But the general behavior is for atomData head to not disappear whenever it's in some other expression.

A couple of other points:

You should use a more specific pattern than _Association. Even _?AssociationQ is better, since it rules out things like Association[1]. But in your case you could define a pattern:

$atomDataPattern = KeyValuePattern[ 
     {"Atom Names" -> {__String}, "Atom Nr" -> {__Integer} }
]

and so then you can use this in function definitions:

f[atomData[ad:$atomDataPattern], args] := f[ad,args]

You can then make a nice formatting for atomData using the functions described in this post.

If you want to program in Mathematica but keep to OOP methods you are used to, you might benefit from the package described in this post.

$\endgroup$
  • $\begingroup$ Utilizing a full OOP package seems a bit overkill right now, but I'll keep them in mind for future. I'm not sure why I didn't make the computed properties as subvalues. Definitely a waste of memory. With regards to upvalues, the inital goal was to make them as general as possible. I wanted it to behave seamlessly as an Association most of the time. The logic being that the number of 'special' functions will be smaller than the number of functions that Associations can work with. But as you've pointed out the other way is probably better. Thank you for the valuable suggestions. $\endgroup$ – void life Jan 8 at 15:14

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.