Take the 2-minute tour ×
Mathematica Stack Exchange is a question and answer site for users of Mathematica. It's 100% free, no registration required.

The impossibility of MatchQ-ing inside and Association MatchQ-ing Associations (MMA 10) explains a problem I have. I would like to convert hierarchical lists of rules to hierarchies of Associations. As an example:

rdata = {"a" -> {"b" -> 1, "c" -> {7, 8, 9}}, "d" -> 3}

Then

rdata /. List -> Association

returns

<|"a" -> <|"b" -> 1, "c" -> Association[7, 8, 9]|>, "d" -> 3|>

where a value that happens to be a list is converted to something meaningless, which is not wanted. I have tried a variety of things like:

rdata /. x : {__Rule} :> Association[x]

or

(rdata /. List -> Association) /. x_Association /; NotAssociationQ[x] :> List @@ x

or

rdata /. {x__Rule} :> Association[x]

but none of them solves the problem.
I finally managed it by picking out the locations of all the lists of Rules, as follows:

rtoA = (# /. {x__Rule} -> Association[x]) &
RulesToAssociation[rdata_] := Module[{pos},
  pos = Drop[#, -1] & /@ Position[rdata, Rule];
  Association @@ MapAt[rtoA, rdata, pos]]
RulesToAssociation[rdata]

which returns the desired result

<|"a" -> <|"b" -> 1, "c" -> {7, 8, 9}|>, "d" -> 3|>

In my real application the rules list comes from the Import of a fairly complicated JSON file. This solution seems to work but I'm not sure how robust it is. Does anyone have a better idea?

share|improve this question

2 Answers 2

There is a ToAssociations function in the GeneralUtilities` package that is perfect for this and for converting nested JSON rules to associations:

Needs["GeneralUtilities`"]
ToAssociations@rdata
(* <|"a" -> <|"b" -> 1, "c" -> {7, 8, 9}|>, "d" -> 3|> *)

This preserves the inner most list that is not a list of rules.


As for your original attempts, the reason a replacement like x : {__Rule} :> Association[x] does not work is because Association is atomic and when /. replaces the outer most list with Association, you lose the ability to "inspect" inside and make replacements.

Instead, what you want is something that replaces inside out, and you can use Replace with the appropriate level spec:

Replace[rdata, x : {__Rule} :> Association[x], {0, Infinity}]
(* <|"a" -> <|"b" -> 1, "c" -> {7, 8, 9}|>, "d" -> 3|> *)
share|improve this answer
    
Nice job. You gave the answer I was going to give and showed me a new function. :-) –  Mr.Wizard Jul 24 at 13:17
    
Thanks a lot. I was trying to figure out how to get Replace to work like that but I was thinking in terms of the /. syntax and it never occurred to me that it would have level specs. There seems to be no mention of the GeneralUtilities` package in the Help Browser, as far as I can tell ? –  John Jowett Jul 25 at 7:30
    
@JohnJowett Yes, it is undocumented at present. –  rm -rf Jul 25 at 10:21
    
ToAssociations do not work for lists with RuleDelayed, I used you second method as Replace[rdata, x : {__RuleDelayed | __Rule} :> Association[x], {0, Infinity}]. Tks! +1 –  Murta Oct 29 at 19:25
    
@rm-rf Please see my answer and remarks concerning the /. replacement mechanism ... How to explain my result ?. –  SquareOne Dec 7 at 17:52

There is another way to get the desired output using /. with this 2-steps approach :

rdata //. p : {__Rule} :> foo @@ p /. foo -> Association
(* <|"a" -> <|"b" -> 1, "c" -> {7, 8, 9}|>, "d" -> 3|> *)

Explanations

First step

Using //. we find at all levels in rdata the occurrences of the target structure {__Rule} that we need to replace in and we temporarily tag it as foo

fs = rdata //. p : {__Rule} :> foo @@ p

One could use a more specific rule instead, for example:

fs = rdata //. p : {Rule[_String, _] ..} :> foo @@ p

It returns a result which has already the correct final structure :

foo["a" -> foo["b" -> 1, "c" -> {7, 8, 9}], "d" -> 3]

If foo is replaced by Association then we'll get the desired output.

Second step

It appears that we can use directly /. to replace foo at all the levels and at the same time:

fs /. foo -> Association
(*<|"a" -> <|"b" -> 1, "c" -> {7, 8, 9}|>, "d" -> 3|>*)

Discussion

This result surprised me because it is in contradiction with the replacement mechanism given in @rm-rf answer :

"the reason a replacement like x : {__Rule} :> Association[x] does not work is because Association is atomic and when /. replaces the outer most list with Association, you lose the ability to "inspect" inside and make replacements"

The two reasons why the replacement in my second step should not work are : 1./ the fact that the replacement is made starting from the outermost level to the inner one and 2./ Association is Atomic

1.Difference between /. foo -> f and /. foo[x_]->f[x]

One must indeed distinguish these two cases :

foo["a", foo["b"], "c"] /. foo -> fff
(* fff["a", fff["b"], "c"] *)

and :

foo["a", foo["b"], "c"] /. foo[x__] -> fff[x]
(* fff["a", foo["b"], "c"] *)

As well explained by @WReach in the comments :

"Example 1 matches foo heads only. As they have no subparts to skip, the inner foo is visited and replaced. Example 2 matches foo[x__] which does have subparts that are skipped, and we can see the inner foo subpart is left unchanged."

which is for me more clear than the documentation of "/." (ReplaceAll) :

enter image description here

Clearly, the fact that I replace Heads only in my second step allow me to make this replacement at all levels.

But it should not work either because ...

2. Association is an atomic expression

which means that it is by definition "locked" in such a way that it can't be modified with tools like /..
For example :

asoc = Association[{a -> x, b -> y, c -> z}]
(* <|a -> x, b -> y, c -> z|> *)

then

asoc /. x->xx
(* <|a -> x, b -> y, c -> z|> *)

shows no replacement is possible inside the Assocation.

This means that my second step shouldn't work also :

foo["a" -> foo["b" -> 1, "c" -> {7, 8, 9}], "d" -> 3] /. foo -> Association
(* <|"a" -> <|"b" -> 1, "c" -> {7, 8, 9}|>, "d" -> 3|> *)

If the outer most foo is replaced first then it should prevent the inner replacement from taking place ... which is not the case.

This probably simply means that the whole expression to be replaced is maintained in an unevaluated form until all replacements are made...

share|improve this answer
1  
We know that matching works from the outermost level inward from the documentation statement that after a match no further rules are tried on that part, or on any of its subparts. Example 1 matches foo heads only. As they have no subparts to skip, the inner foo is visited and replaced. Example 2 matches foo[x__] which does have subparts that are skipped, and we can see the inner foo subpart is left unchanged. Examples 3 and 4 give the same results as examples 1 and 2 respectively. Sow/reap tells us nothing here since the values are all being sown before replacement occurs. –  WReach Dec 8 at 3:47
1  
To see this all in action, constrast foo[2, foo[1], 3] /. e:foo[x__] /; (Print[e];True) :> fff[x] to Replace[foo[2, foo[1], 3], e:foo[x__] /; (Print[e]; True) :> fff[x], {0, Infinity}] –  WReach Dec 8 at 3:48
    
@WReach Thank you for your clear explanations. I modified my post (and also removed my shameful mistake concerning Sow :(() –  SquareOne Dec 8 at 11:38

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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