Why does b_set_dict need the key to exist?

SWI-Prolog -- Dicts: structures with named arguments says:

b_set_dict(+Key, !Dict, +Value)
Destructively update the value associated with Key in Dict to Value. The update is trailed and undone on backtracking. This predicate raises an existence error if Key does not appear in Dict. The update semantics are equivalent to setarg/3 and b_setval/2.

Why does the key need to exist? If I had a state object that needed to be passed across many predicates that occasionally update it, then this kind of undone-on-backtracking in-place update would be very useful. But if the key has to be preexisting in order for the in-place update to work, that limits the usability of the feature, requiring the top level to break encapsulation by preparing the keys in advance.

Because dicts are in the end syntactic sugar for a Prolog compound term (with a name that is not a normal atom). We cannot enlarge the term in-place. The today discussed “open dict” pack uses an indirection through an attributed variable to bypass this. Another solution would be to have a container term that holds the dict itself. The you can create a new dict and use setarg/3 to update the container.

Makes sense.

Do you mean a dictionary with a single key–value pair, where the value is the actual dictionary to update “in place”? I think with the right utilities, it could work. The API surface should still be straightforward, we’re talking about a state object that gets passed around for many predicates to use, some of which may update it. It should also be unwound automatically during backtracking. The goal is to avoid passing a mutable state object around as two arguments.

%% update_state
% Path is dot-separated, as with a.b.c.
put(stateWrapper{state: State}, Path, NewValue) :-
  put_key(...)

If this is what you mean, I think it’s doable if there are atom-splitting utilities available to us.

For example. The idea is to check whether the actual dict has the key. If so, update it. Else, add a key and update the container with the new dict.

I don’t really see why you need that. But atomic_list_concat/3 can be used to split atoms on some separator.

There’s another, convenient method (creating a new dictionary): put_dict/4

?- D=d{a:2}, put_dict(a, D, 5, D1).
D = d{a:2},
D1 = d{a:5}. % Has "updated" key

?- D=d{}, put_dict(a, D, 5, D1).
D = d{},
D1 = d{a:5}. % Has added the key