Solving this problem efficiently is not trivial for a beginner. Assuming that the elements of the list are ground, we can start by noticing that sorting the list will cluster together all elements that shared the first argument in the obj/2
compound term. For example:
| ?- sort([obj(x,y),obj(x,z),obj(a,b),obj(b,c)], S).
S = [obj(a, b), obj(b, c), obj(x, y), obj(x, z)]
yes
The sort/2
is a standard built-in predicate. Any decent Prolog system should implement it with complexity O(n*log(n)). After sorting, we can walk the list, which we can so in O(n) to filter it:
filter(List, Filtered) :-
sort(List, Sorted),
walk(Sorted, Filtered).
walk([], []).
walk([obj(X,Y)| Sorted], Filtered) :-
walk(Sorted, X, obj(X,Y), Filtered).
walk([], _, Element, [Element]).
walk([obj(X,_)| Sorted], X, _, Filtered) :-
!,
delete(Sorted, X, Rest),
walk(Rest, Filtered).
walk([obj(X,Y)| Sorted], _, Element, [Element| Filtered]) :-
walk(Sorted, X, obj(X,Y), Filtered).
delete([], _, []).
delete([obj(X,_)| Sorted], X, Rest) :-
!,
delete(Sorted, X, Rest).
delete(Rest, _, Rest).
Sample call:
| ?- filter([obj(x,y),obj(x,z),obj(a,b),obj(b,c)], Filtered).
Filtered = [obj(a, b), obj(b, c)]
yes
Looks good but we should do more comprehensive testing. We can define a property that all the filter/2
predicate solutions must satisfy:
property(List, Filtered) :-
filter(List, Filtered),
% all elements of the output list must
% be in input list
forall(
member(X, Filtered),
member(X, List)
),
% no two elements in the output list
% should share the first argument
\+ (
select(obj(X,_), Filtered, Rest),
member(obj(X,_), Rest)
),
% all elements in the input list whose
% first argument is not repeated must
% be in the output list
\+ (
select(obj(X,Y), List, Rest),
\+ member(obj(X,_), Rest),
\+ member(obj(X,Y), Filtered)
).
We can now use a property-based testing implementation such as Logtalk's lgtunit
QuickCheck implementation. But there's a catch. Property-based testing requires that we be able to generate lists with obj/2
elements. The solution, we cheat! First we do a syntactic transformation from obj(X,Y)
to X-Y
. This transformation doesn't change the semantics of the predicate being tested:
filter(List, Filtered) :-
sort(List, Sorted),
walk(Sorted, Filtered).
walk([], []).
walk([X-Y| Sorted], Filtered) :-
walk(Sorted, X, X-Y, Filtered).
walk([], _, Element, [Element]).
walk([X-_| Sorted], X, _, Filtered) :-
!,
delete(Sorted, X, Rest),
walk(Rest, Filtered).
walk([X-Y| Sorted], _, Element, [Element| Filtered]) :-
walk(Sorted, X, X-Y, Filtered).
delete([], _, []).
delete([X-_| Sorted], X, Rest) :-
!,
delete(Sorted, X, Rest).
delete(Rest, _, Rest).
We apply the same syntactic transformation to the property/2
predicate:
property(List, Filtered) :-
filter(List, Filtered),
% all elements of the output list must
% be in input list
forall(
member(X, Filtered),
member(X, List)
),
% no two elements in the output list
% should share the first argument
\+ (
select(X-_, Filtered, Rest),
member(X-_, Rest)
),
% all elements in the input list whose
% first argument is not repeated must
% be in the output list
\+ (
select(X-Y, List, Rest),
\+ member(X-_, Rest),
\+ member(X-Y, Filtered)
).
We can now test using the goal:
| ?- lgtunit::quick_check(
property(
+list(pair(char,char)),
-list(pair(char,char))
)
).
% 100 random tests passed
% starting seed: seed(25256,26643,1563)
yes
Note: in the definition of the property/2
predicate, we assume that the de facto standard member/2
and select/3
list predicates are available in user
(i.e. at the top-level interpreter). If that's not the case, prefix their calls with list::
.