[comp.lang.prolog] Elegance=efficiency, the continuing story.

ok@quintus (07/20/88)

I saw (never mind where) the following definition today:

/*1*/	permute(X, [A|Z]) :-
		select(A, X, Y),
		permute(Y, Z).
	permute([], []).

where select/3 is the usual

	select(Item, [Item|Set], Set).
	select(Item, [OtherItem|Set0], [OtherItem|Set]) :-
		select(Item, Set0, Set).

This version of permute/2 looked ugly to me.
(1) From Clocksin & Mellish 1st edition I learned to put base cases
    _first_.  It is _much_ the clearest place to put them.
(2) It isn't obvious from the code that the induction is really on
    the *first* argument of permute/2: if the first argument is unbound
    permute/2 may fail to terminate even if the second argument is ground.

A version which has neither of these defects is

/*2*/	permute([], []).		% to permute [], return []
	permute([X|Xs], Ys1) :-		% to permute [X|Xs],
		permute(Xs, Ys),	% permute Xs giving Ys
		select(X, Ys1, Ys).	% insert X anywhere in Ys giving Ys1

Now, I think this version is much easier to read.
The reason for the title of this message is that it turns out to be
considerably faster in Quintus Prolog:  4 times faster for tiny lists,
5 times faster for 10 element lists.  I can explain the result after
the event, but I wasn't expecting anywhere near that big a difference.
Elegance pays!

Of course, that version still suffers from the well known problem that
it can't be used to solve for the first argument given the second.
But at least it is a bit less surprising that that is so.  In fact we
can easily fix that bug and produce a satisfactory permute/2:

/*2*/	permute(Xs, Ys) :-
		permute(Xs, Ys, Ys).

	permute([], [], []).
	permute([X|Xs], Ys1, [_|Zs]) :-
		permute(Xs, Ys, Zs),
		select(X, Ys1, Ys).

This is a bit less than twice as slow as version /*2*/, but it is
more than twice as fast as version /*1*/ and works both ways around.
[permute(Xs, Ys, Zs) checks that Xs and Zs are the same length, so
that if Ys is known, it will terminate.]

tarau@ouareau.iro.umontreal.ca (Paul Tarau) (08/07/88)

The following is an 'all_permutation' deterministic Horn clause
program I wrote some time ago, when trying (without full success)
to automatically convert nondeterministic programs to their
deterministic all-solutions counterparts.

all_permutations([],[[]]).
all_permutations([X|Xs],Perms2):-
	all_permutations(Xs,Perms1),
	extend_permutations(Perms1,X,[],Perms2).

extend_permutations([],_,Perms,Perms).
extend_permutations([Perm|Perms],X,Perms1,[[X|Perm]|Perms3]):-
	insert_item(Perm,X,[],Perms2,Perms3),
	extend_permutations(Perms,X,Perms1,Perms2).

insert_item([],_,_,Perms,Perms).
insert_item([Y|Ys],X,Acc,Perms1,[Zs|Perms2]):-
        reverse_and_append(Acc,[Y,X|Ys],Zs),
	insert_item(Ys,X,[Y|Acc],Perms1,Perms2).

reverse_and_append([],Acc,Acc).
reverse_and_append([X|Xs],Acc,Zs):-
	reverse_and_append(Xs,[X|Acc],Zs).

The practical interest of this kind of (very space-consuming) programs
is to be able to compare alternative solutions within a pure Prolog
framework. We can actually reduce the size of the generated list by
inserting some form of filtering at each inductive step.

The program uses the same kind of 'natural' induction on the first
argument as Richard O'Keefe's 'permute'.

It is about 8 times faster then an equivalent 'bagof' solution
(on a SUN-3,Quintus 2.2 with a list of 7 elements).

However, I think it is not as fast as it could be. For example
it might be a better way to extend permutations of the previous
inductive step then the use of 'append_and_reverse'.

Does someone know about a faster and/or shorter 'all_permutations'
program?

Paul Tarau