How do I combine partial results from several tasks into the fullest Dict?
← Common Collection Tasks · Ref: Q1406
Merge each task's Dict into one with ':~:'. Merge never overwrites a set value; it fills whatever is unset or missing, key by key - so the first value recorded for a key wins and every gap is filled.
KEY BY KEY
- a key the target lacks is added
- a value the target holds unset is filled from a set source value
- a value the target holds set is kept
GATHERING
gathered <- Dict() of (String, String) gathered :~: {"name": "Ada", "role": String()} gathered :~: {"name": "Grace", "role": "engineer"} stdout.println($gathered)
The first task recorded the name but not the role; the second supplies the role, and its name is ignored.
ORDER MATTERS
Because the first recorded value wins, merging A then B can differ from merging B then A when both set the same key. Merge the most trusted source first.
NOT THE SAME AS +
'+' builds a new Dict and, where keys overlap, the right-hand value is used. ':~:' changes the target and keeps what it already has.
TAKEN MEANS SHARED, NOT COPIED
What the target takes - a key it lacked, a value it held unset - is the same object the source holds (shallow, as a List's ':=:' is). Because merge recurses, a LATER merge into a structure the target took also changes the source's inner object:
taskOne <- {"person": {"name": "Ada"}} taskTwo <- {"person": {"role": "engineer"}} combined <- Dict() of (String, Dict of (String, String)) combined :~: taskOne //takes taskOne's own inner Dict combined :~: taskTwo //merges into that same Dict - taskOne now has the role too stdout.println($taskOne) stdout.println($combined)
To keep every source independent, give the target its own fresh inner structure first, so each merge fills the target's Dict rather than taking a source's:
keptApart <- {"person": Dict() of (String, String)}
See Q1405 for the rule on single values. See Q1408 for Dicts nested inside Dicts. See Q186 for merging Lists and Dicts.
Example
defines module qa.collectiontasks.mergegatherpartialresults defines constant PERSON <- "person" NAME <- "name" ROLE <- "role" defines function personTask() -> field as String found as String <- rtn as Dict of (String, Dict of (String, String)): {PERSON: {field: found}} fieldOf() -> results as Dict of (String, Dict of (String, String)) field as String <- rtn as String: results.getOrDefault(PERSON, Dict() of (String, String)).getOrDefault(field, "none") taskResults() <- rtn as List of Dict of (String, String): [ {"name": "Ada", "role": String()}, {"name": "Grace", "role": "engineer"}, {"city": "London"} ] defines program MergeGatherPartialResultsDemo() stdout <- Stdout() //Each task found some fields; merge them all for the fullest set gathered <- Dict() of (String, String) for task in taskResults() gathered :~: task for entry in gathered stdout.println(`${entry.key()}: ${entry.value()}`) //The first value recorded for a key wins, so order matters first <- {"colour": "red"} second <- {"colour": "blue"} firstThenSecond <- Dict() of (String, String) firstThenSecond :~: first firstThenSecond :~: second stdout.println(`first then second: ${firstThenSecond.getOrDefault("colour", "none")}`) secondThenFirst <- Dict() of (String, String) secondThenFirst :~: second secondThenFirst :~: first stdout.println(`second then first: ${secondThenFirst.getOrDefault("colour", "unknown")}`) //A key the target lacked is TAKEN - the source's own inner Dict, shared not copied who <- "Ada" job <- "engineer" taskOne <- personTask(NAME, who) taskTwo <- personTask(ROLE, job) combined <- Dict() of (String, Dict of (String, String)) combined :~: taskOne combined :~: taskTwo stdout.println(`task one shared, its role: ${fieldOf(taskOne, ROLE)}`) stdout.println(`combined: ${fieldOf(combined, NAME)} ${fieldOf(combined, ROLE)}`) //Give the target its own fresh inner Dict first: each merge fills it, the sources stay apart taskThree <- personTask(NAME, who) taskFour <- personTask(ROLE, job) keptApart <- {PERSON: Dict() of (String, String)} keptApart :~: taskThree keptApart :~: taskFour stdout.println(`task three kept apart, its role: ${fieldOf(taskThree, ROLE)}`) stdout.println(`kept apart gathered: ${fieldOf(keptApart, NAME)} ${fieldOf(keptApart, ROLE)}`)
Common mistakes
E50060 — EK9's Dict has no putAll() method; ':~:' is the merge, and unlike putAll() it never overwrites a value the target already holds. See ek9 -h E50060 for details.
Incorrect:
gathered.putAll(task)
Correct:
gathered :~: task
Other ways to ask this
- Each task found some of the fields - how do I gather them into one Dict?
- Which value wins when two Dicts I merge both have the key?
- How do I fill the gaps in one Dict from another without losing what it has?
- Does the order I merge Dicts in matter?
- Does merge copy the nested Dicts it takes, or share them with the source?
Coming from another language?
Java: map.putAll(other) overwrites existing keys; gathering 'first found wins' needs putIfAbsent in a loop, and a null value is not filled. Python: dict.update overwrites; {**b, **a} keeps a's values but builds a new dict. Kotlin: map + other lets other win. EK9: target :~: source keeps every set value and fills missing keys and unset values in place, recursing into nested Dicts.
Keywords: partial results, merge, dict, shallow merge, putIfAbsent, shared not copied, :~:, gather, fill gaps, first recorded wins, merge order