Does ':~:' merge a Dict of Dict all the way down, or replace the inner Dict?
← Common Collection Tasks · Ref: Q1408
It merges all the way down. ':~:' never overwrites a set value; it fills whatever is unset or missing, element by element, recursing into compatible nested structures.
RECURSION
When the target and the source both hold a set value under the same key, and the source's value is the same kind of structure as the target's (a Dict in a Dict, a List in a Dict, a CSV in a Dict), the source's value is merged INTO the target's by the same rule: missing keys added, unset values filled, set values kept, Lists appended. This repeats at every level.
INCOMPATIBLE NESTING
When the two values are not the same kind of structure - a Dict on one side and a String on the other - the target's set value is kept whole. Merge never forces a different type in.
SHARED, NOT COPIED
A value the target takes from the source (a key it lacked) is the same object as the source's, as with a Dict's ':=:' (its values are shared). Merge into it later and the source sees the change. To keep every source independent, give the target its own fresh inner Dict before merging, so each merge fills the target's Dict instead of taking a source's (Q1406 runs both).
See Q1406 for gathering partial results. See Q1405 for the rule on single values. See Q1409 for Lists and Sets.
Example
defines module qa.collectiontasks.mergenesteddicts defines program MergeNestedDictsDemo() stdout <- Stdout() //Three levels: user -> settings -> display saved <- {"user": {"display": {"theme": "dark"}}} defaults <- {"user": {"display": {"theme": "light", "font": "mono"}, "locale": {"language": "en"}}} //Recurse: theme is kept, font is filled, the locale Dict is added saved :~: defaults user <- saved.getOrDefault("user", Dict() of (String, Dict of (String, String))) display <- user.getOrDefault("display", Dict() of (String, String)) locale <- user.getOrDefault("locale", Dict() of (String, String)) stdout.println(`theme: ${display.getOrDefault("theme", "?")}`) stdout.println(`font: ${display.getOrDefault("font", "-")}`) stdout.println(`language: ${locale.getOrDefault("language", "none")}`) stdout.println(`settings level keys: ${length user}`) //A Dict on one side and a String on the other: the target keeps its value whole //(values of different types share only Any, so each Dict declares its value type) mixed as Dict of (String, Any): {"settings": {"mode": "fast"}, "label": "plain"} incoming as Dict of (String, Any): {"settings": "flat text", "label": {"nested": "dict"}} mixed :~: incoming stdout.println(`incompatible kept: ${mixed}`)
Other ways to ask this
- How does merge treat nested dictionaries?
- What happens when I merge a Dict whose values are Dicts?
- What if the same key holds a Dict on one side and a String on the other when I merge?
- Is EK9 merge a deep merge?
Coming from another language?
Java: Map.putAll replaces an inner map whole; a deep merge needs a recursive helper or a library. JavaScript: Object.assign and spread are shallow; lodash merge is deep. Python: dict.update is shallow. EK9: ':~:' is the deep merge, and it keeps every set value instead of overwriting it.
Keywords: merge, dict of dict, incompatible nesting, nested dict, :~:, deep merge, recursive merge