5

The question on making a record like in Mathematica has been discussed in few places, such as Struct data type in Mathematica?.

The problem with all these methods, is that one loses the ability, it seems, to do the specific extra check on each argument, as in when one does x_?NumericQ.

My question is: Is there a way in Mathematica to make a record or a struct, and yet be able to use the checking as above on the individual elements?

I am trying to settle down on one method to use as I am tired of having functions called with 10 parameters on them (sometimes one can't avoid this), even when I try to make each function very specific, to minimze the number of parameters, some functions just need many parameters to do the specific job.

First I show the three methods I know about.

Method 1

foo[p_]:=Module[{},
    Plot[Sin[x],{x,from/.p,to/.p}]
]
p={from->-Pi,to->Pi};
foo[p]

Advantage: safe, as if I change the symbol 'from' to something else, it will still work. As the following example.

foo[p_]:=Module[{},
    Plot[Sin[x],{x,from/.p,to/.p}]
]
p={from->-Pi,to->Pi};
from=-1; (* By accident the symbol from was set somewhere. It will work*)
foo[p]

Method 2

Clear[p,foo];
foo[p_]:=Module[{},
    Print[p];
    Plot[Sin[x],{x,p["from"],p["to"]}]
]
p["from"] = -Pi;
p["to"]   = Pi;

foo[p]

Advantage: also safe, strings are immutable. Do not have to worry about the "from" value changing. But having strings everywhere is not too readable?

Method 3

Clear[p,to,from];
foo[p_]:=Module[{},
    Plot[Sin[x],{x,p[from],p[to]}]
]
p[from] = -Pi;
p[to]   = Pi;

foo[p]

Disadvantage: if any of the symbols 'from' or 'to' get overwritten somewhere, will cause problem, as in

from=-4; (*accidentally the symbol from is assigned a value*)
foo[p]   

So. I think method (1) is the most safe. But now I lose the ability to do this:

foo[from_?NumericQ, to_?NumericQ] := Module[{},
    Plot[Sin[x], {x, from, to}]
]
from = -Pi; to = Pi;
foo[from, to]

So, I am hoping to get an idea to be able to combine making a 'record' like, but at the same time, still be able to use the parameter checking on individual elements in the record? Or is this question is not not well posed for Mathematica functional/rule based programming style?

That is one thing I wish Mathematica had, which is a real record to help manage and organize all the variables used in the program.

Community
  • 1
  • 1
Nasser
  • 12,849
  • 6
  • 52
  • 104
  • 1
    There is some discussion about simulating structure/record types buried in this question: [What is the difference between Mathematica Rules and the objects returned by GraphEdit](http://stackoverflow.com/q/6848385/211232). – WReach Sep 09 '11 at 13:05

2 Answers2

10

First, I'd like to mention that all the methods you listed are IMO flawed and dangerous. The main reason why I don't like them is that they introduce implicit dependences on global variables (the reasons why this is bad are discussed e.g. here), and can also mess up with the scoping. Another problem of them is that those approaches look like they won't scale nicely to many instances of your structs existing simultaneously. The second method you listed seems the safest, but it has its problems as well (strings as field names, no way to type-check such a struct, also symbols used there may accidentally have a value).

In my post here I discussed a possible way to build mutable data structures where methods can do extra checks. I will copy the relevant piece here:

Unprotect[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
ClearAll[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
Module[{first, second},
   first[_] := {};
   second[_] := {};
   pair /: new[pair[]] := pair[Unique[]];
   pair /: new[pair[],fst_?NumericQ,sec_?NumericQ]:= 
      With[{p=new[pair[]]}, 
          p.setFirst[fst];
          p.setSecond[sec];
          p];
   pair /: pair[tag_].delete[] := (first[tag] =.; second[tag] =.);
   pair /: pair[tag_].setFirst[value_?NumericQ] := first[tag] = value;
   pair /: pair[tag_].getFirst[] := first[tag];
   pair /: pair[tag_].setSecond[value_?NumericQ] := second[tag] = value;
   pair /: pair[tag_].getSecond[] := second[tag];       
];
Protect[pair, setFirst, getFirst, setSecond, getSecond, new, delete]; 

Note that I added checks in the constructor and in the setters, to illustrate how this can be done. More details on how to use the structs constructed this way you can find in the mentioned post of mine and further links found there.

Your example would now read:

foo[from_?NumericQ, to_?NumericQ] :=
   Module[{}, Plot[Sin[x], {x, from, to}]];
foo[p_pair] := foo[p.getFirst[], p.getSecond[]]
pp = new[pair[], -Pi, Pi];
foo[pp]

Note that the primary advantages of this approach are that state is properly encapsulated, implementation details are hidden, and scoping is not put in danger.

Community
  • 1
  • 1
Leonid Shifrin
  • 22,449
  • 4
  • 68
  • 100
  • Leonid, some day I hope to see the code for a fairly large *Mathematica* application that puts together a number of your concepts, so that I may see how your methods affect "programming in the large." – Mr.Wizard Oct 25 '11 at 23:37
  • @Mr.Wizard Depends on what you'd call fairly large, but I do have a few such under development. Some day, I will publish them. – Leonid Shifrin Oct 26 '11 at 04:03
  • Note that the [`Association`](http://reference.wolfram.com/language/ref/Association.html) structure added in Mathematica 10 essentially solves this problem. See [my answer](http://stackoverflow.com/a/33465328/330202). – Jess Riedel Nov 01 '15 at 18:04
  • @JessRiedel I disagree. It only solves part of the problem. And in any case, I don't see the point of this comment: do you really think I was not aware of Associations, or how they can be used in this context? – Leonid Shifrin Nov 01 '15 at 18:40
  • Yes, I figured you were not aware of `Associations` when you wrote and last edited this post in Sept 2011 because `Associations` were not introduced until Mathematica 10. What part of Nasser's original question does using `Associations` (or `DataSet`) not solve? – Jess Riedel Nov 01 '15 at 19:08
  • @JessRiedel Nasser wants to be able to do argument checks based on patterns, as usually done in Mathematica. My answer shows how to do this. With Associations, there is no easy way to do the same. Anyway, my general opinion is that Associations solve only a part of the problem. You can use them in a struct-like manner, but if you want to attach methods, not just fields, then you lose the richness of Mathematica pattern-matching. I don't dispute that Associations are useful in this context, of course. – Leonid Shifrin Nov 01 '15 at 19:14
  • OK, I've update my answer to make it clear how argument checks are done using `Association`. In my opinion, this is much easier and safer than delving into `Protect`, especially for the limited context of Nasser's question. Outside that context you might like the ability to define a standard types of struct, check whether a given struct is of that type, etc. Associations can't do that easily because they are atomic, but my intuition is that the overhead and risks of using your technique will almost never be worth it. I'd be interested in a link to a problem where you think it is. Cheers! – Jess Riedel Nov 01 '15 at 21:20
0

Mathematica 10 has introduced Association, which has many of the most important properties of a struct (and has similar syntax to the replacement rules you've been experimenting with).

plotLimits = <| "lowerLimit" -> -Pi, "upperLimit" -> Pi |>; 
(*this is the syntax for an Association[]*)

foo[p_]:=Module[{},
 Plot[Sin[x],{x,p["lowerLimit"],p["upperLimit"]}]
];
(* assoc["key"] is one of many equivalent ways to specify the data *)

We can also easily implement checks on the arguments

fooWithChecks[p_?(NumericQ[#["lowerLimit"]] && NumericQ[#["upperLimit"]] &)] := Module[{}, 
 Plot[Sin[x], {x, p["lowerLimit"], p["upperLimit"]}]
];

In this case, foo[plotLimits] and fooWithChecks[plotLimits] give the same plot, because plotLimits has nice numerical values. But if we define

badPlotLimits = <|"lowerLimit" -> bad, "upperLimit" -> Pi|>;

then evaluating foo[badPlotLimits] gives an error

Plot::plln: Limiting value bad in {x,<|lowerLimit->bad,upperLimit->2 \[Pi]|>[lowerLimit],<|lowerLimit->bad,upperLimit->2 \[Pi]|>[upperLimit]} is not a machine-sized real number. >>
Plot[Sin[x], {x, <|"lowerLimit" -> bad, "upperLimit" -> 2 \[Pi]|>["lowerLimit"], <|"lowerLimit" -> bad, "upperLimit" -> 2 \[Pi]|>["upperLimit"]}]

but evaluating fooWithChecks[badPlotLimits] just remain unevaluated since the argument doesn't pass the NumericalQ check:

fooWithChecks[<|"lowerLimit" -> bad, "upperLimit" -> 2 \[Pi]|>]

It's not clear to me why you ask about the form foo[from_?NumericQ, to_?NumericQ] rather than foo[p_?(someCheckFunction)]. A key benefit of having the struct in the first place is that you can reorganize how the struct is stored in memory, say by swapping the order of "lowerLimit" and "upperLimit", without re-writing any of the functions that use it (since they call it by p["lowerLimit"] not p[[1]]). That ability breaks if you define foo such that, when foo is called, the arguments are inferred by order. (In other words, you are preventing foo from knowing about the structure.) You can still do it, of course, perhaps because you want to use foo on non-structs too:

foo[from_?NumericQ, to_?NumericQ] :=
 Module[{}, Plot[Sin[x], {x, from, to}]];
foo[p] := foo[p["lowerLimit"], p["upperLimit"]];

If you wanted to be really careful, you could use this:

foo[p_?(SubsetQ[Keys[#],{"lowerLimit", "upperLimit"}]&)] :=
 foo[p["lowerLimit"], p["upperLimit"]];

Unfortunately, you can't gives names to certain Association patterns (which would be the Association analog of this technique for lists) using something like this

plotLimitType=<|"lowerLimit"->_NumericQ, "upperLimit"->_NumericQ|>

because Associations are atomic(ish). See here.

By the way, note that the keys like "lowerLimit" don't need to be in quotes. Using this style

plotLimits = <|lowerLimit -> -Pi, upperLimit -> Pi|>;

works just as well.


For more info, see

Community
  • 1
  • 1
Jess Riedel
  • 336
  • 3
  • 15