4
$\begingroup$

So I've been toiling away on my DataFrame package. I've been trying to get Set to work with it, but it seems it either can't be done (ie via UpValues), or is ill advised. So I decided to bite the bullet and just roll my own custom DFSet function. But I can't figure out how the get the evaluation to work like it does when you Set the Part of something.

This is my DFSet function:

DFSet[Part[DataFrame[DFMetadata[metastore_], DFData[store_]], name_String], data_List] :=
    (
        If[
            Length@data =!= metastore["Length"], 
            Abort[]
        ];
        If[
            !MemberQ[metastore["Names"], name], 
            metastore["Names"] = Append[metastore["Names"], name]
        ];
        store[name] = Developer`ToPackedArray@data;
    )

The idea is that it would be called like this:

DFSet[frame[["a"]], {1, 2, 3}]

The problem is that if I run it as is the Part bit will try to evaluate and that won't do. But if I set the attribute HoldFirst on DFSet frame won't evaluate to its OwnValue.

A test case with code:

(****Code needed for a working test case but not part of the problem****)
(*Helper functions*)
StringVectorQ[x_] := MatchQ[x,{__String}]
TopMatrixQ[x_] := Length[Union@(Length/@x)]==1
(*Constructor*)
DataFrame[names_?StringVectorQ, data_?TopMatrixQ] := 
    Module[
        {store, metastore, length},
        If[Length@First@data =!= Length@names, Abort[]];
        store = Unique[DFstore];
        metastore = Unique[DFmetastore];
        metastore["Length"] = Length@First@Transpose@data;
        metastore["Names"] = names;
        Thread[
            Set[#1,#2]&[store/@names, Developer`ToPackedArray/@Transpose@data]
        ];
        DataFrame[DFMetadata[metastore], DFData[store]]
    ] 
(*Pretty Formatting*)
Format[DataFrame[DFMetadata[metastore_], _DFData]] := 
    "DataFrame"[Length[metastore["Names"]]]
(*UpValue for Part*)
DataFrame /: Part[DataFrame[DFMetadata[metastore_], DFData[store_]], name_String] := 
    (
        If[!MemberQ[metastore["Names"], name], Abort[]];
        store[name]
    )
(**************************)
(****The actual problem****)
(*DFSet function*)
DFSet[Part[DataFrame[DFMetadata[metastore_], DFData[store_]], name_String], data_List] :=
    (
        If[
            Length@data =!= metastore["Length"], 
            Abort[]
        ];
        If[
            !MemberQ[metastore["Names"], name], 
            metastore["Names"] = Append[metastore["Names"], name]
        ];
        store[name] = Developer`ToPackedArray@data;
    )
(*******************)
(****A test case****)
(*Arrange*)
frame = DataFrame[{"a","b"},{{1,2},{1,2},{1,2}}]
(*Act*)
DFSet[frame[["c"]],{3,3,3}]
DFSet[frame[["b"]],{4,4,4}]
(*Assert*)
frame[["c"]]=={3,3,3}
frame[["b"]]=={4,4,4}
(****Output from successful test should be:
Out=DataFrame[2]
Out=True
Out=True
*)
$\endgroup$
3
  • $\begingroup$ My apologies but I'm not reading all that code. I understand the heart of the matter to be evaluating an arbitrary symbol frame in frame[["a"]] but not Part, within your DFSet function. Is that correct? $\endgroup$
    – Mr.Wizard
    Commented Feb 20, 2013 at 21:21
  • $\begingroup$ Yes, that is correct. The code isn't there for wading through, but so that one can have a complete test case to copy/paste. $\endgroup$
    – Mr Alpha
    Commented Feb 20, 2013 at 21:27
  • $\begingroup$ Please review my answer and tell me if it solves your problem. $\endgroup$
    – Mr.Wizard
    Commented Feb 20, 2013 at 21:31

2 Answers 2

4
$\begingroup$

I'm not trying to read and understand all of your code but I understand the heart of the matter to be evaluating an arbitrary symbol frame in frame[["a"]] but not Part, within your DFSet function.

To that end you could do something like this:

ClearAll[DFSet, DFSetInner]

SetAttributes[{DFSet, DFSetInner}, HoldFirst]

DFSet[LHS_, RHS_] := Block[{Part}, DFSetInner[#, RHS] & @ LHS]

frame = DataFrame[{"a", "b"}, {{1, 2}, {1, 2}, {1, 2}}]

DFSet[frame[["a"]], {1, 2, 3}]
DFSetInner[DataFrame[{"a", "b"}, {{1, 2}, {1, 2}, {1, 2}}][["a"]], {1, 2, 3}]

You would then move the primary definition to DFSetInner.


Leonid raises a concern about the unintended consequences of Blocking Part. It seemed to me that unless DataFrame itself relied upon Part that this was reasonable, and I wanted to allow for things such as:

x := frame[["a"]];

DFSet[x, {1, 2, 3}]

which do not work with Leonid's present definition. If one prefers not to Block Part in this way another approach is:

DFSet[obj_[[part__]], RHS_] := DFSetInner[#[[part]], RHS] & @ obj

This no longer works with x above and requires a literal Part in the LHS. (HoldFirst must be maintained for both symbols as it was set above.)

Also, if one does not wish to introduce another Symbol DFSetInner you can attach the original definition to the same symbol with a different syntax as Leonid did. (I felt it best to keep them separate since I was not including the full definition in my answer.) You could also drop the "syntactic sugar" of Part in the inner definition of you so choose. Additionally if following this method we can leverage the order of parameters and the HoldFirst attribute to automatically evaluate obj in the normal course of evaluation:

SetAttributes[DFSet, HoldFirst]

DFSet[obj_[[parts__]], RHS_] := DFSet[{parts}, obj, RHS]

DFSet[{name_String}, DataFrame[DFMetadata[metastore_], DFData[store_]], data_List] := . . .

This still does not work with x := frame[["a"]]; DFSet[x, {1, 2, 3}]. If such behavior is desired one might extend the definition using my step function.

SetAttributes[DFSet, HoldFirst]

DFSet[obj_[[parts__]], RHS_] := DFSet[{parts}, obj, RHS]

DFSet[other_, RHS_] := step[other] /. _[x_] :> DFSet[x, RHS]

DFSet[{name_String}, DataFrame[DFMetadata[metastore_], DFData[store_]], data_List] := . . .

Now DFSet[x, {1, 2, 3}] works without the unintended consequences of Blocking Part.

frame = DataFrame[{"a", "b"}, {{1, 2}, {1, 2}, {1, 2}}];

x := frame[["a"]];

DFSet[x, {1, 2, 3}]
$\endgroup$
11
  • $\begingroup$ All cool and also +1, but I don't understand why you stick to Part for DFSetInner. All that matters is to recognize when part assignments are used with Part, but why carry Part along to the r.h.s.? It's not really needed any more, it is just syntactic sugar here. $\endgroup$ Commented Feb 20, 2013 at 21:56
  • $\begingroup$ @Leonid That's a good point, but it's Mr Alpha's syntactic sugar and I wanted to respect it. If it's easier for him to think in terms of Part then I thought it best to leave that in the definition. (Speaking to the OP) One could use e.g. DFSet[obj_[[part__]], RHS_] := DFSet[#, {part}, RHS] & @ obj too if that sugar is not valued in the inner definition. $\endgroup$
    – Mr.Wizard
    Commented Feb 20, 2013 at 22:00
  • $\begingroup$ I see. But that's not needed for the inner function, in your approach. All that matters is that the public function has it. $\endgroup$ Commented Feb 20, 2013 at 22:02
  • $\begingroup$ @Leonid okay, I'll follow that advice in the last lines in my answer. $\endgroup$
    – Mr.Wizard
    Commented Feb 20, 2013 at 22:03
  • 1
    $\begingroup$ I came to realize that your step - based solution is very cool. It actually represents a general method. I think that we now have the ability to perform pattern-matches on partially evaluated expressions, which is a pretty powerful capability. If we expect an argument, part of which we don't want to evaluate, we simply pattern-match the head we don't want to evaluate, and use your construct to evaluate the argument until the intermediate value has this head (in which case some special instructions apply), or until the argument gets fully evaluated. I think this technique has great potential. $\endgroup$ Commented Feb 21, 2013 at 1:01
6
$\begingroup$

The suggested solution

There is a simple option - add these definitions:

SetAttributes[DFSet, HoldFirst];
DFSet[Part[df_, name_String], data_List] := DFSet[df, name, data];

DFSet[df_, name_String, data_List] := 
 With[{dfl = df},
   DFSet[dfl, name, data] /; dfl =!= Unevaluated[df]
 ];

and all will be fine. Basically, what happens here is that we only maintain the HoldFirst attribute for the 2-argument definition to recognize Part before it evaluates, and destructure it. The second definition replaces the first argument by its evauated form, in case when it is different (a check is needed to avoid infinite recursion).

Note, that these definitions must be executed before the existing ones for DFSet, since the order of them is important here. Note also, that we could not get away with just having a single DFSet[Part[df_, name_String], data_List] := DFSet[Evaluate@df, name, data], because DFSet is now HoldFirst, and a stand-alone application of the 3-argument DFSet would not pattern-match correctly when we pass variables like frame, rather than explicit DataFrame objects.

Using references to cover more cases

Mr.Wizard rightly noted that my current solution does not cover cases such as

x := frame[["a"]]

where indeed DFSet[x, {1, 2, 3}] returns unevaluated. There are several ways to cover this case. One is to suppy some custom one-step evaluator like Mr.Wizard's step function. One potential problem with fixed-number-of-steps evaluators is that there is generally no way to tell how many evaluation steps would it take to evaluate some part of expression which is of interest to us. In this particular case though, this is not of a concern for Mr.Wizard's solution, because the definitions in his solution using step are constructed such that the one-step evaluation will repeat until one obtains Part[...], so this is certainly a perfectly valid and clever solution.

In any case, here I will show another method, which is - introduce and use generic references. Such references would work also for functions which hold their arguments, and effectively will allow local one-step evaluation. Here is a possible definition for a reference generator:

ClearAll[ref];
SetAttributes[ref, HoldAll];
ref /: (op : (Set | SetDelayed))[ref[sym_Symbol], rhs_] :=
     sym /: (f : Except[
               Clear | ClearAll | Unset
              | Remove | Set | SetDelayed
            ])[left___, sym, right___] :=
  f[left, rhs, right];

Here is then the syntax to defined a reference:

ref @ y := frame[["b"]]

I defined ref in this way intentionally, so that the definition looks natural: all we need to do is to add ref @ to the left of our usual variable assignment. Now, for example, we get:

Hold[y]

(* Hold[frame[["b"]]] *)

One can also test that part assignments will now work:

DFSet[y, {1, 2, 3}]
frame[["b"]]

(* {1,2,3} *)

Note that such references can be useful in a more general context, since in combination with functions having Hold-attributes they implement local one-step evaluation. Since UpValues for these references will be applied by evaluator before DownValues for the functions where we pass them, we get the desired effect. Their values are substituted into those functions but we don't lose control over further evaluation of these values. Such evaluation is governed by the definitions of those functions we pass these values to.

$\endgroup$
6
  • 1
    $\begingroup$ Leonid, since you think things through to a degree I usually cannot, what is the benefit of this method which requires two additional lines of code over my method that requires only one? $\endgroup$
    – Mr.Wizard
    Commented Feb 20, 2013 at 21:38
  • $\begingroup$ @Mr.Wizard I don't introduce a new head, and I don't have to Block Part - who knows what unwanted consequences the latter may have. Actually, I think that my solution (particularly the last one) is conceptually simpler than yours, even if more verbose. And I personally value conceptual simplicity more than saving a couple of lines of code (not to detract from your solution!). $\endgroup$ Commented Feb 20, 2013 at 21:40
  • $\begingroup$ Well, I don't need to introduce a new symbol either if I change the arguments of DFSet, I just thought it was easier to answer if I kept them separate. Regarding the second point what problem do you foresee if DataFrame does not itself rely on Part to expand it's definition? $\endgroup$
    – Mr.Wizard
    Commented Feb 20, 2013 at 21:43
  • 1
    $\begingroup$ @Mr.Wizard But that's exactly the point - it may rely on it, or it may call other sub-functions which rely on it. Even if this does not happen now, it may happen in the future after some refactoring. And I bet it would be quite hard to debug this one if the code base becomes large - dynamic scoping is notoriously hard to debug, because the changes are not local to a given piece of code. $\endgroup$ Commented Feb 20, 2013 at 21:46
  • $\begingroup$ @Mr.Wizard Also, I think that using dynamic scoping here is conceptually somewhat lame, since we are here dealing with an explicitly lexical and syntactic problem. Basically, Part is used here purely syntactically, its semantics for DataFrame is completely overridden, so I think that nit allowing it to evaluate at all in this part assignment is the better approach. $\endgroup$ Commented Feb 20, 2013 at 21:51

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.