9
$\begingroup$

Accessing parts of an Association can be done either using Part or the [] extraction notation. Although similar, there are subtle differences and I'm wondering if (immutable) key removal is part of that subtlety or set to be updated. For example, changing a key value is similar

assoc = <|"a" -> 1, "b" -> 2|>;
assoc["a"] = 0;
assoc

(* <|"a" -> 0, "b" -> 2|> *)

since analogously

assoc = <|"a" -> 1, "b" -> 2|>;
assoc[["a"]] = 0;
assoc

(* <|"a" -> 0, "b" -> 2|> *)

but removing a key-value is apparently not since while

assoc = <|"a" -> 1, "b" -> 2|>;
assoc["a"] =.;
assoc

(* <|"b" -> 2|> *)

analogously

assoc = <|"a" -> 1, b -> 2|>;
assoc[["a"]] =.;
assoc

(* Unset::write: Tag Part in <|a->1,b->2|>[[a]] is Protected. >>
  $Failed
  <|"a" -> 1, b -> 2|>
 *)
$\endgroup$
14
  • $\begingroup$ I can see how this Unset behaviour might be different for Association compared with a List given the former is a "symbolically indexed list" with a different notion of mutability (n.b. removing a list element in this way also fails since no particular symbol is being unset) but I also tend to think that it is perhaps natural to expect similar behaviour. $\endgroup$ Aug 4, 2015 at 23:49
  • $\begingroup$ I am actually surprised that Unset works for the [ ] syntax for assocs - I wasn't aware of that. While I can understand that it has some syntactical nicety, I don't feel that this sits well with the standard sematnics of Unset, which previously was used strictly for global rules. This looks like a case where syntax wins over semantics, and a rather questionable design decision, in my view. $\endgroup$ Aug 5, 2015 at 9:40
  • $\begingroup$ Awareness can be a funny thing as I myself actually first noticed this comparing one of your answers that itself compared itself with a previous answer that did just this. $\endgroup$ Aug 5, 2015 at 13:14
  • $\begingroup$ You are totally right. Somehow I wasn't surprised at that point, while I should've been. Actually, with Unset, this isn't clear-cut case to me. We may try to think of an Unset as of reverse to Set or SetDelayed. Now, Set serves two purposes: 1. create global rules (like SetDelayed, but immediate), and 2. Efficient in-place modifications of parts of expressions stored in symbols, when used with Part. The first use in general and conceptual. The second use is special, and comparatively low-level. I think it is rather unfortunate that the same operator (Set) is used in both cases... $\endgroup$ Aug 5, 2015 at 14:23
  • $\begingroup$ Here is what I would consider a cleaner design, in terms of semantics: Set would only create rules, while there would be a separate SetPart for in-place part modifications. Now, Unset would be exact opposite of Set, that is, used only to destroy global rules. Now, if we think about it, using Unset on a part makes no sense. You can either delete a part, or modify it, but not unset. So, logically, we'd need a DeletePartFrom operation, which actually exists for assocs (KeyDropFrom). But, such operation can't be general, because general expressions are based on sequences, and .. $\endgroup$ Aug 5, 2015 at 14:28

1 Answer 1

4
$\begingroup$

While perhaps appearing as peripheral language design issue, I think clarifying the varieties of in-place, L-assignment further illuminates the much more consequential notion of mutability/immutability within the WL.

My conclusion is that yes the difference noted between [] and [[]] assignments (both in Unset and more generally) is somewhat incoherent with the current [] implementation representing the most consistent, in-place interpretation. Accordingly, extending this interpretation to all other varieties would add coherency (A "bug" label where in-place assignment doesn't align with []'s idiom however, doesn't seem warranted so I'll instead label such deviations, a "mite").

Let's start with Lists by way of background:

    ls = {a, b, c};
    ls[[2]] = B;
    ls
 (*
    {a, B, c}
 *)

As pointed out by Leonid Shifron in the question comments, Set is being used in two subtly different ways here; in the first instance a variable is being created; in the second instance a part is being modified. While not keeping these usages separate lies behind subsequent differences (in this periphery at least), IMO an overriding imperative here is pedagogy; the use of Set and its infix form "=" is so ingrained, its application so universal, that the described dual usage is both natural and an integral part of Mathematica's learning curve. Further, existing coherency levels bear enhancement as to be outlined.

Firstly note that the creative usage of Set affects WL's evaluation sequence in addition to (or as part of) defining a new structure:

    << GeneralUtilities` (* needed for Definitions *)
    Definition@ls
    Definitions@ls
(*
    ls = {a, B, c} 
    {HoldPattern[ls] :> {a, B, c}}
*)

but as Set giveth, Unset taketh

    ls =.
    Definition@ls
    Definitions@ls
(*
    Null 
    {}
*)

This Unsetusage at a global or "whole structure" level can be viewed as both undoing Set's actions or else removing a structure. In the latter case, given Set's dual usage, coherency posits that a similar part removal should also be available - in other words can one taketh away part of a structure using this syntax? Alas and unfortunately, for List's the answer is no (although the situation is different with Associations as we will soon see)

    ls = {a, b, c}
    ls[[2]] =.
    ls
(*
    {a, b, c} 
    Unset::write: Tag Part in {a,b,c}[[2]] is Protected. >>
    $Failed
    {a, b, c}
*)

but I regard this as another "mite". I maintain that it is perfectly reasonable and natural to expect that instead {a,c} be returned given ls[[2]]=.'s removal semantics. Conceptually I don't see too great a distance between removing a whole structure from a namespace and removing a part (while recognizing that the former is also accompanied with the removal of a rule). We can view these operations through the lens of a coherency matrix where we observe that the outstanding action not yet considered is the creative action of Set on parts (an implementation of which ensures that UnSet consistently undo's Set's actions).

enter image description here

This suggests that

    ls = {a, b, c}
    ls[[4]] = d;
    ls
(*         
    Set::partw: Part 4 of {a,b,c} does not exist. >>
    {a, b, c}
*)

could instead return {a,b,c,d} (which, that it doesn't, I regard as another "mite").

When it comes to Associations however, all the coherency just described is nicely implemented as we can observe by stepping through each cell in the coherency matrix.

enter image description here

IMO, a gain in coherency would result from squishing all outstanding mites by extending []'s Set semantics to ls[[]], assoc[[]] and dataset[].

Note that in relation to creative part applications of Set, previous examples relied on the part extending the structure "continuously". Discontinuous part creations require the insertion of missing elements semantics for which are already provided by Missing[] and SparseArrays. For example,

  ls = {a, b, c};
  ls[[5]] = e;
  ls
(*
  {a, b, c, Missing[], e}
*)

Currently, this insertion does not occur even for assoc[] since while

    assoc = <|1 -> a, 2 -> b, 3 -> <|4 -> c|>|>;
    assoc[3, 5] = d
    assoc
 (*
    d
    <|1 -> a, 2 -> b, 3 -> <|4 -> c, 5 -> d|>|> 
 *)

we have that

   assoc = <|1 -> a, 2 -> b, 3 -> <|4 -> c|>|>;
   assoc[3, 5, 2] = d;
   assoc

(* Set::kval: The value for the key 5 does not exist. >>
   <|1 -> a, 2 -> b, 3 -> <|4 -> c|>|>
 *)

We return to the connection between immutability and mutability. This exposition and suggested unification of Set's semantics, is by no means intended to emphasise or promote it as the dominant or even preferred method for the frequent changing of data structures. Set is ideal for beginners, for initiating structures, for changing single elements in the simple immutable fashion and finally for recording chains of changes. There are, of course, more flexible functions for handling richer mutations. For example, Set can't readily handle changes that involve a function of the part's value as provided by MapAt.

    assoc = <|1 -> a, 2 -> b, 3 -> c|>;
    MapAt[Capitalize@ToString@# &, assoc, 2]
(*
    <|1 -> a, 2 -> "B", 3 -> c|>
*)

nor can it, for example, singly handle multiple modifications in the manner of ReplacePart

    assoc = <|1 -> a, 2 -> b, 3 -> c|>;
    ReplacePart[assoc, {1 -> "A", 3 -> "C"}]
(*
    <|1 -> "A", 2 -> b, 3 -> "C"|>
*)

Finally, Set doesn't enjoy the ready compositions via operator forms within a Query

    assoc = <|1 -> a, 2 -> b, 3 -> c|>;
    Query[MapAt[Capitalize@ToString@# &, 2]@*ReplacePart[{1 -> "A", 3 -> "C"}]]@assoc
(* 
    <|1 -> "A", 2 -> "B", 3 -> "C"|>
*)

Not only are these richer modifications but they are also done so mutably with the base definition of assoc remaining unaffected. This is a powerful and important feature of the WL that is, IMO, essential to encouraging and facilitating seamless experimentation. In fact, as touched on elsewhere the interplay between mutable/immutable structures captures scientific practice and even the unfolding of evolution. More locally, much of Mathematica's prototyping ability IMO stems from WL's mutable nature but structures eventually need augmentation to record finds, a fact that I think explains recurring questions on immutable modifications. Currently, a whole/global Set is required

  assoc = <|1 -> a, 2 -> b, 3 -> c|>;
  assoc = Query[MapAt[Capitalize@ToString@# &, 2]@*ReplacePart[{1 -> "A", 3 -> "C"}]]@assoc
(*
   <|1 -> "A", 2 -> "B", 3 -> "C"|>
*)

Eventually repeated application of this construction starts to invoke "boilerplateness". For some users it may also be a little disconcerting minimally changing a large legacy data structure by apparently, expensively copying the entire structure (a misconception not perhaps easily disabused for those less versed in mutable/immutable distinctions). But is there a more natural, infix approach to immutable assignment? I think so. We can take our cue from AddTo or TimesBy, a+=1 or b*=2 whereby the content immediately wrapping the = defines an operator set to operate on the data structure defined by the first symbol.

IMO, the relevant, "infix operator" begging for a call to action here is "@="; behold the "naturalness" of the following:

assoc @= MapAt[Capitalize@ToString@# &, 2];
assoc @= ReplacePart[2 -> B];
assoc @= ReplacePart[{1 -> "A", 3 -> "C"}];
assoc @= Query[MapAt[Capitalize@ToString@# &, 2]@* ReplacePart[{1 -> "A", 3 -> "C"}]];

Its generality can be seen how existing idioms get covered

a += 1;
a @= # + 1 &

or in an answer to dataset in-place assignment

dataset[Select[#age > 30 &], "salary"] *= 2
dataset @= Query[All, MapAt[If[#age > 30, 2#&, Identity], #, "salary"] &]

or by "rendering redundant" existing immutable variants such as AppendTo,PrependTo, KeyDropFrom (while reducing the need for future variants )

assoc = <|1 -> a, 2 -> b, 3 -> c|>;

assoc @= Append[4 -> d];
assoc @= KeyDrop[2]

This is yet another example of the power of operator forms. Note that in these immutable versions there is usually a sense of one large, legacy structure, being augmented by adding/changing a single element. Hence operator forms, which require a natural target structure to operate on, naturally emerge by considering the original structure as constituting such a target. Other functions, such as Join, on the other hand, have traditionally considered joinable elements to have equal prominence so that without an obvious target no such operator form emerges. The notion of an immutable, infix assignment alters this whereby the following becomes more natural:

assoc = <|1 -> a, 2 -> b, 3 -> c|>;
assoc @= Join[<|4 -> d|>];

We can see how common immutable, infix assignment improves code readability and data structure tracking

capitalizeMiddle = MapAt[Capitalize@ToString@# &, 2];
replaceBookends = ReplacePart[{1 -> "A", 3 -> "C"}];
logicallyExtend = Join[<|4 -> d, 5 -> e|>]; (* no current operator form *)
capIt = Prepend[0 -> z];
drop3 = KeyDrop@3;
addSuffix = StringJoin["Suffix"]  (* no current operator form *)

assoc @= capitalizeMiddle;
assoc @= replaceBookends;
assoc @= Query[capitalizeMiddle@*replaceBookends];
assoc @= logicallyExtend;
assoc @= capIt;
assoc @= drop3;
st @= addSuffix;

Currently there does not exist such an infix operator, @=, and while it could be introduced by the Notation utility, general package development doesn't gel with such integration; we can however, currently implement it by instead borrowing an existing, if mildly related, infix operator, \[DotEqual]

 SetAttributes[DotEqual, HoldFirst];
 DotEqual[a_, b_] := a = b@a; 

     assoc = < 1 -> a, 2 -> b, 3 -> c|>;
     j = 4;
     {
       assoc \[DotEqual] capitalizeMiddle,
       assoc \[DotEqual] capIt,
       assoc \[DotEqual] drop3,
       j \[DotEqual] (2#&)
      };
     {assoc, j}
(*
     {<|0 -> z, 1 -> a, 2 -> "B"|>, 8}
*)

whose visual sense is better provided by an actual visual

enter image description here

For Join/StringJoin, operator forms need defining (since as mentioned, in this idiom's absence, no target operand presents) and not wanting to tinker with core, native definitions we can instead create o operator forms when these are not built-in

oJoin[h_] := Function[{expr}, Join[expr, h], HoldAll];
oStringJoin[h_] := Function[expr, StringJoin[expr, h], HoldAll];

logicallyExtend = oJoin[<|4 -> d, 5 -> e|>];
addSuffix = oStringJoin@"Suffix";

    z = {1}; st = "stem";
    z \[DotEqual] logicallyExtend;
    st \[DotEqual] addSuffix;
    {z, st}
(* 
    {<|0 -> z, 1 -> a, 2 -> "B"|>, 8}
*)

Again visually

enter image description here

Set naturally sets things off in mutable, experimental playgrounds; it then helps record interesting finds, something more naturally done with immutable, infix assignment.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.