Setting up a “struct” in Mathematica safely

前端 未结 2 681
情歌与酒
情歌与酒 2021-01-03 04:12

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, i

相关标签:
2条回答
  • 2021-01-03 04:38

    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

    • Association
    • DataSet (which builds on Association)
    • Wolfram guides: Elementary introduction to DataSets and Fast introduction for programmers.
    • Mathematica.SE questions: How to make use of Associations and Struct equivalent in Mathematica?
    • This HackerNews comment.
    0 讨论(0)
  • 2021-01-03 04:39

    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.

    0 讨论(0)
提交回复
热议问题