In general one works around the poor selection of fundamental data types in Prolog the standard way: by adding libraries and using interfaces. SWI-Prolog, for example, comes with the assoc
library that implements an AVL tree-based association data structure. (As an aside, balanced trees are more common in functional and logic programming than hash tables because it's easier to create "persistent" data structures on trees than hash tables—persistent in the FP sense of sharing internal structure.)
Using this library looks something like this:
?- [library(assoc)].
% library(assoc) compiled into assoc 0.00 sec, 97 clauses
true.
?- empty_assoc(Assoc).
Assoc = t.
?- empty_assoc(Assoc), get_assoc(test, Assoc, V).
false.
?- empty_assoc(Assoc), put_assoc(test, Assoc, foo, Assoc2).
Assoc = t,
Assoc2 = t(test, foo, -, t, t).
?- empty_assoc(Assoc),
put_assoc(test, Assoc, foo, Assoc2),
get_assoc(test, Assoc2, Value).
Assoc = t,
Assoc2 = t(test, foo, -, t, t),
Value = foo.
Once you have something that gives you an interface like this, you can define all kinds of logical relations on top of it. Once you have logical relations, Prolog's normal unification machinery will take care of the rest—no special support for this or that data type is required. Based on your requirements, I think what you want is like a subset relation, except checking both that all of one association are in the other and they all have the same value. I guess that would look something like this:
association_subset(Left, Right) :-
forall(gen_assoc(Assoc, Left, Value), get_assoc(Assoc, Right, Value)).
This predicate will only be true if the Left association is a subset of the Right association, as defined above. We can test it and see if it's doing what we want:
simple(Assoc) :-
empty_assoc(Empty),
put_assoc(foo, Empty, foo_test, V1),
put_assoc(bar, V1, bar_test, Assoc).
complex(Assoc) :-
simple(Assoc1),
put_assoc(baz, Assoc1, bazzle, Assoc).
unrelated(Assoc) :-
empty_assoc(Empty),
put_assoc(baz, Empty, bazzle, Assoc).
...
?- simple(X), complex(Y), association_subset(X, Y).
X = t(foo, foo_test, <, t(bar, bar_test, -, t, t), t),
Y = t(baz, bazzle, -, t(bar, bar_test, -, t, t), t(foo, foo_test, -, t, t)).
?- simple(X), simple(Y), association_subset(X, Y).
X = Y, Y = t(foo, foo_test, <, t(bar, bar_test, -, t, t), t).
?- simple(X), unrelated(Y), association_subset(X, Y).
false.
?- complex(X), simple(Y), association_subset(X, Y).
false.
We can translate this to your exact question like so:
left(Assoc) :-
empty_assoc(Empty),
put_assoc(foo, Empty, 1, Assoc).
right(Assoc) :-
left(Assoc1),
put_assoc(bar, Assoc1, 2, Assoc).
?- left(L), right(R), association_subset(L, R), get_assoc(foo, L, Q).
L = t(foo, 1, -, t, t),
R = t(foo, 1, <, t(bar, 2, -, t, t), t),
Q = 1.
I realize that this answer doesn't really answer the question you asked, but I hope it answers the question beneath the question. In other words, there doesn't need to be special support for these data structures—the above predicate could be defined over association lists as well, you can see that all you'd need is the usual ways of making empty associations, adding, testing for, and generating the keys/values of the association and the underlying data structure becomes irrelevant. No special support is necessary either data-structure-wise or unification-wise. Special syntax would certainly make it nicer to look at! But it isn't necessary to get the behavior you desire.
?- attr(A, [foo-1, bar-2]), attr(A,[foo-Q]).
and get backQ=1
. the impl'n:attr(A, [N-V|R]):- memberchk( N-X, A), X=V, attr(A, R).
;attr(_, []).
that's all. – Derisible