Apply function to nested associations

This seems like it should be easy but I am trying to concisely enhance some nested associations with Mathematica objects (e.g., convert things to matrices, etc) but I'm having a bit of difficulty with nested items. In my case I want to go through an arbitrarily nested association and anywhere there is a key == 'somevalue' (correlation below) I want to replace its value with the result. KeyValueMap almost does the trick (see below). But it doesn't take a levelspec, I'm wondering if someone can show me how you get around this since some functions take levelspecs and some don't. Example code:

test = <| "name" -> "foo", "correlation" -> {{1, 2}, {3, 4}} ,
"descendants" -> <| "name" -> "bar", "correlation" -> {{12, 13}, {20, 22}}|> |>;
KeyValueMap[If[#1 == "correlation", #1 -> MatrixForm[#2], #1 -> #2] &, test]


Expected output would have both correlations formatted as matrices, but as you can guess the descendant one is missed with the code as is.

You can use Replace[] on Associations, but it's sort of weird because it tries to treat keys transparently. If your Replace patterns include association markers (<| |>), then Replace can be convinced to care about keys, but the tricky part there is matching against every rule within an association.

It turns out that you can get past all of this and coax Mathematica into doing what you want with a clever pattern.

Replace[test, <|a___, "correlation" -> x_, b___|> :>
<|a, "correlation" -> MatrixForm[x], b|>, All]


We're asking Replace to match all associations that have a "correlation" rule anywhere inside (using the pattern <|a___, "correlation" -> x_, b___|>) at All nesting levels within your association.

The a___ (three underscores) and b___ parts capture everything within the Association before or after the "correlation" rule. The part after the :> in the replace rule drops your transformed middle part (captured by x) in between what was captured as a and b.

• WReach's and yours both work. I like that this one seems to be the most concise, but I don't quite understand it. Do you think you could add a little more explanation (e.g., what is 'a' doing here?). Thank you! – Dan Nov 3 '16 at 16:24
• @Dan, he could have renamed a -> pre and b -> post. That's a pattern expression used so "correlation" can appear anywhere in the Association, at the beginning, in the middle, or at end. – alancalvitti Nov 3 '16 at 19:50
• @Dan, I added more explanation above. – sblom Nov 3 '16 at 19:55
• @sblom, do you know why levelspec All matches both "correlation" Keys but numerical levelspec 1 matches only the {{12,13},{20,22}} value, not the first? Is there a numerical levelspec that matches both? – alancalvitti Nov 3 '16 at 19:56
• @alancalvitti Part 0 refers to the head of an expression. Level 0 refers to the whole expression. The documentation for Replace tells us that a number n matches levels 1 through n whereas All matches all levels including zero. – WReach Nov 4 '16 at 3:49

Any occurrence of the association key "correlation" at any depth of an expression will have a position index that matches the pattern {___, Key["correlation"]}. We can use either MapIndexed or ReplacePart to exploit this observation and apply a function of our choosing at such positions (e.g., MatrixForm):

MapIndexed[
#2 /.  {{___, Key["correlation"]} :> MatrixForm[#], _ -> #} &
, test
, -1
]


ReplacePart[
test
, i : {___, Key["correlation"]} :> RuleCondition @ MatrixForm @ Extract[test, i]
]


RuleCondition is used in the latter case because an association value is a held context. See (29319) for details of this method.

• If anyone at WRI is listening, it might be useful if there were a "vertical" level-spanning notation that matched any number of levels, e.g. expr[[SpanLevels, Key["x"]]] or expr // Query[SpanLevels, "x"]. This would be an analog to the XQuery/XPath // operator. – WReach Nov 3 '16 at 15:43
• Thank you so much for these answers. They also worked for me and I'm sure I will find opportunities to use these approaches as well. – Dan Nov 4 '16 at 3:34

You may use MapAt if you don't mind explicitly specifying the Keys.

MapAt[MatrixForm, test, {{"correlation"}, {"descendants", "correlation"}}]


If "descendants" contains a list of Associations then {"descendants", All, "correlation"} instead.

Hope this helps.