27

I've only been working with Prolog for a couple days. I understand some things but this is really confusing me.

I'm suppose to write a function that takes a list and flattens it.

?- flatten([a,[b,c],[[d],[],[e]]],Xs).  
Xs = [a,b,c,d,e].                           % expected result

The function takes out the inner structures of the list.

This is what I have so far:

flatten2([],[]).
flatten2([Atom|ListTail],[Atom|RetList]) :-
      atom(Atom), flatten2(ListTail,RetList).
flatten2([List|ListTail],RetList) :-
      flatten2(List,RetList).

Now, this works when I call:

?- flatten2([a,[b,c],[[d],[],[e]]], R).
R = [a,b,c,d,e].                         % works as expected!

But when I call to see if a list that I input is already flattened, is returns false instead of true:

?- flatten2([a,[b,c],[[d],[],[e]]], [a,b,c,d,e]).
false.                                   % BAD result!

Why does it work on one hand, but not the other? I feel like I'm missing something very simple.

mat
  • 39,707
  • 3
  • 42
  • 68
ToastyMallows
  • 3,975
  • 5
  • 40
  • 50
  • With this specific task, please also consider a more general case: What should `?- flatten([X], Ls).` yield? You may think that it "obviously" should yield `Ls = [X]`. However, you then have the following problem: `?- flatten([X], Ls), Ls = [X], X = [a].` *succeeds*, **but** if we simply exchange the goals by commutativity of conjunction, we get: `?- Ls = [X], X = [a], flatten([X], Ls).`, or more compactly, `?- flatten([[a]], [[a]]).`, which of course *must fail* because `[[a]]` is not a flat list. So, which is it? Fail or succeed? This shows that this is really not a nice relation at all. – mat May 07 '15 at 08:39
  • This is why I recommend you take a look at `append/2`. It limits this relation to a more meaningful and often also more practically useful version. – mat May 07 '15 at 08:55

7 Answers7

25

The definition of flatten2/2 you've given is busted; it actually behaves like this:

?- flatten2([a, [b,c], [[d],[],[e]]], R).
R = [a, b, c] ;
false. 

So, given the case where you've already bound R to [a,b,c,d,e], the failure isn't surprising.

Your definition is throwing away the tail of lists (ListTail) in the 3rd clause - this needs to be tidied up and connected back into the list to return via RetList. Here is a suggestion:

flatten2([], []) :- !.
flatten2([L|Ls], FlatL) :-
    !,
    flatten2(L, NewL),
    flatten2(Ls, NewLs),
    append(NewL, NewLs, FlatL).
flatten2(L, [L]).

This one recursively reduces all lists of lists into either single item lists [x], or empty lists [] which it throws away. Then, it accumulates and appends them all into one list again out the output.

Note that, in most Prolog implementations, the empty list [] is an atom and a list, so the call to atom([]) and is_list([]) both evaluate to true; this won't help you throw away empty lists as opposed to character atoms.

  • You're right it was busted. I don't know why I was getting the right answer before. I understand how your approach works but how does it get rid of empty lists? Also, why is `[]` an atom? – ToastyMallows Jan 30 '12 at 06:31
  • 1
    @ToastyMallows it gets rid of `[]`s because appending a list and an `[]` gets you your same list back. `[]` is both atom and list for historical reasons. Look up "cons" and "nil". `[]` is what's known in LISP as "nil". – Will Ness Jan 30 '12 at 18:24
  • (I am new to prolog) What does the ! stand for? I had the same solution, but without ! it does not work – FranXh May 05 '14 at 22:46
  • 2
    `!` is a special character called a _cut_ in Prolog. It tells the interpreter to cut (ignore) other choices to prevent backtracking. For more information, [Learn Prolog Now!](http://www.learnprolognow.org/lpnpage.php?pagetype=html&pageid=lpn-htmlse43) has a nice tutorial. –  May 05 '14 at 23:24
8

You can maintain your lists open-ended, with both a pointer to its start, and an "ending hole ⁄ free pointer" (i.e. logvar) at its end, which you can then instantiate when the end is reached:

flatten2( [], Z, Z):- !.                                        % ---> X
flatten2( [Atom|ListTail], [Atom|X], Z) :-                      %      .
    \+is_list(Atom), !,                                         %      .
    flatten2( ListTail, X, Z).                                  %      Y
flatten2( [List|ListTail], X, Z) :-                             %      .
    flatten2( List,     X, Y),       % from X to Y, and then    %      .
    flatten2( ListTail, Y, Z).       % from Y to Z              %      Z --->

You then call it as

flatten2( A, B):- flatten2( A, B, []).

That way there's no need to use reverse anywhere. This technique is known as "difference lists", but it's much easier just to think about it as "open-ended lists" instead.


update: This is much easier coded using the syntax. Since it is unidirectional (the first argument must be fully ground), why not use cuts after all:

flattn([]) --> [], !.
flattn([A|T]) --> {\+is_list(A)}, [A], !, flattn(T).
flattn([A|T]) --> flattn(A), flattn(T).

Testing:

16 ?- phrase(flattn([a,[b,c],[[d],[],[e]]]), [a, b, c, d, e]).
true.

17 ?- phrase(flattn([a,[b,c],[[d],[],[e]]]), R).
R = [a, b, c, d, e].

18 ?- phrase(flattn([a,[b,X],[[d],[],[e]]]), [a, b, c, d, e]).
X = c.

If the definition were fully declarative, the last one should've succeeded also with X=[c] ; X=[[],c] ; ... ; X=[[c]] ; ...; alas, it isn't.

(edit2: simplified both versions, thanks to @mat's comments!)

Community
  • 1
  • 1
Will Ness
  • 62,652
  • 8
  • 86
  • 167
  • Technically I like your solution best, but it didn't work for me in SWI-Prolog 6. – FK82 Oct 10 '14 at 11:05
  • I tried `flatten2([1,[8,3],[3,[5,6],2],8], X).` and it returned `false.` – FK82 Oct 10 '14 at 11:19
  • @FK82 you're right, I should've used `atomic/1` instead of `atom/1`. -- fixed it, thanks! – Will Ness Oct 10 '14 at 11:30
  • 1
    `!//0` is a DCG nonterminal, there is no need to wrap it. For example, you can write `flattn([]) --> !.` However, you *must* use `phrase/2` to portably access a DCG! An implementation is free to choose any expansion method it wants, and indeed need not expand the DCG to explicit differences at all. For example, in some abstract machines, using a different argument order may be a lot more efficient. If you rely on a particular expansion method, it will be increasingly harder to implement such optimizations in future systems. – mat Jan 17 '17 at 12:11
  • I have upvoted this because the DCG version is the first version of the predicate in this whole thread that I think I have a chance to understand ;-) You can still simplify it though! – mat Jan 17 '17 at 16:13
2

Here's an accumulator based version for completeness:

% flatten/2
flatten(List, Result) :- flatten(List, [], Result).

% auxiliary predicate flatten/3
flatten([], Result, Result).
flatten([Head| Tail], Part, Result) :- 
    is_list(Head),
    !, 
    flatten(Head, HR),
    append(Part, HR, PR),
    flatten(Tail, PR, Result).
flatten([Head| Tail], Part, Result) :- 
    append(Part, [Head], PR),
    flatten(Tail, PR, Result).
flatten(X, Part, Result) :-
    fail.
repeat
  • 19,449
  • 4
  • 51
  • 152
FK82
  • 4,517
  • 3
  • 25
  • 36
  • usually we try to avoid `append`, unless it's O(1), like with e.g. difference lists, `app(A-B,B-C,A-C).`. – Will Ness Oct 10 '14 at 11:12
  • @WillNess Yeah, well I'm new to Prolog. :-) I tried to avoid append but couldn't get it to work using lists only. – FK82 Oct 10 '14 at 11:23
  • nicely done. :) (you didn't do the usual flatten, flatten, append - you tried to make at least one recursive call as a tail call; good). -- BTW, a clause that always `fail`s can be safely removed altogether - whether it matches a clause's head and immediately fails, or just fails because there was no (any more) matches, doesn't matter: a fail is a fail. – Will Ness Oct 10 '14 at 11:38
  • @WillNess Thank you, noted! :) – FK82 Oct 10 '14 at 11:56
1

Building on if_//3 and list_truth/2, we can implement myflatten/2 as follows:

myflatten(Xs,Ys) :-
   phrase(myflatten_aux(Xs),Ys).

myflatten_aux([]) --> [].
myflatten_aux([T|Ts]) --> 
   if_(neither_nil_nor_cons_t(T), [T], myflatten_aux(T)),
   myflatten_aux(Ts).

:- use_module(library(dialect/sicstus/block)).

:- block neither_nil_nor_cons(-).
neither_nil_nor_cons(X) :-
   \+nil_or_cons(X).

nil_or_cons([]).
nil_or_cons([_|_]).

neither_nil_nor_cons_t(X,Truth) :-
   (  nonvar(X)
   -> (  neither_nil_nor_cons(X) -> Truth = true
      ;                             Truth = false
      )
   ;  nonvar(Truth) 
   -> (  Truth == true -> neither_nil_nor_cons(X)
      ;  Truth == false,  nil_or_cons(X)
      )
   ;  Truth = true,  neither_nil_nor_cons(X)
   ;  Truth = false, nil_or_cons(X)
   ).

Sample queries (taken from other answers, and comments to answers):

?- myflatten([[4],[[5,6],[7,[8],[9,[10,11]]]]], Xs).
Xs = [4, 5, 6, 7, 8, 9, 10, 11].

?- myflatten([1,[8,3],[3,[5,6],2],8], Xs).
Xs = [1, 8, 3, 3, 5, 6, 2, 8].

?- myflatten([a,[b,c],[],[[[d]]]], Xs).
Xs = [a, b, c, d].

?- myflatten([a,[b,c],[[d],[],[e]]], Xs).
Xs = [a, b, c, d, e].

neither_nil_nor_cons_t and not(nil_or_cons_t) describe have same solutions, but the solution order differs. Consider:

?- myflatten([A,B,C],Xs), A=a,B=b,C=c.
A = a,
B = b,
C = c,
Xs = [a, b, c] ;                       % does not terminate universally
Community
  • 1
  • 1
repeat
  • 19,449
  • 4
  • 51
  • 152
1

Prolog's list notation is syntactic sugar on top of very simple prolog terms. Prolog lists are denoted thus:

  1. The empty list is represented by the atom []. Why? Because that looks like the mathematical notation for an empty list. They could have used an atom like nil to denote the empty list but they didn't.

  2. A non-empty list is represented by the term .\2, where the first (leftmost) argument is the head of the list and the second (rightmost) argument is the tail of the list, which is, recursively, itself a list.

Some examples:

  • An empty list: [] is represented as the atom it is:

    []
    
  • A list of one element, [a] is internally stored as

    .(a,[])
    
  • A list of two elements [a,b] is internally stored as

    .(a,.(b,[]))
    
  • A list of three elements, [a,b,c] is internally stored as

    .(a,.(b,.(c,[])))
    

Examination of the head of the list is likewise syntactic sugar over the same notation:

  • [X|Xs] is identical to .(X,Xs)

  • [A,B|Xs] is identical to .(A,.(B,Xs))

  • [A,B] is (see above) identical to .(A,.(B,[]))

Myself, I'd write flatten/2 like this:

%------------------------
% public : flatten a list
%------------------------
flatten( X , R ) :-
  flatten( X , [] , T ) ,
  reverse( T , R )
  .

%--------------------------------------------
% private : flatten a list into reverse order
%--------------------------------------------
flatten( [] , R , R ) .        % the empty list signals the end of recursion
flatten( [X|Xs] , T , R ) :-   % anything else is flattened by
  flatten_head( X , T , T1 ) , % - flattening the head, and
  flatten( Xs , T1 , R )       % - flattening the tail
  .                            %

%-------------------------------------
% private : flatten the head of a list
%-------------------------------------
flatten_head( X , T , [X|T] ) :- % if the head is a not a list
  \+ list(X) ,                   % - simply prepend it to the accumulator.
  ! .                            %
flatten_head( X , T , R     ) :- % if the head is a list
  flatten( X , T , R )           % - recurse down and flatten it.
  .

%-----------------------
% what's a list, anyway?
%-----------------------
list( X ) :- var(X) , ! , fail .
list( []    ) .
list( [_|_] ) .
Nicholas Carey
  • 60,260
  • 12
  • 84
  • 126
  • I tried `flatten([a,[b,c],[],[[[d]]]],X)` call with your code and it didn't work. The atom-handling case seems missing in your version. – Will Ness Jan 31 '12 at 20:17
  • but now it produces `X = [a, [c, b], [[[d]]]]`. – Will Ness Feb 01 '12 at 09:47
  • what does list( X ) :- var(X) , ! , fail . mean? – bph Jul 08 '15 at 11:48
  • 1
    `list(X) :-var(X) , ! , fail.` say, "If `X` is an unbound variable, X is *not* a list." The `!, fail.` bit eliminates the choice point (so it can't follow the predicate's other alternative paths) and fails. With that that guard clause, an unbound variable would unify with `[]` and succeed. And on backtracking, it would unify (again) with `[_|_]` and succeed a second time. A true/false check for list-ness should be deterministic. – Nicholas Carey Jul 08 '15 at 17:26
0

I didn't find a solution using findall, so I'll add it. (it will work if the list is ground)

First, we define how to test for a list:

list(X) :- var(X), !, fail.
list([]).
list([_|_]).

and the transitive closure of member, we call it member*:

'member*'(X, Y) :- member(X, Y).
'member*'(X, Y) :- member(Z, Y), 'member*'(X, Z).

The flattened list is all the solution of member* which are not lists:

flatten(X, Y) :- findall(Z, ('member*'(Z, X), \+ list(Z)), Y).

Example:

?- flatten([[4],[[5,6],[7,[8],[9,[10,11]]]]],Y).
Y = [4, 5, 6, 7, 8, 9, 10, 11].
fferri
  • 15,633
  • 3
  • 36
  • 67
0

Without any other predicate, with tail-recursion only.

flatten([[X|S]|T], F) :- flatten([X|[S|T]], F).
flatten([[]|S], F) :- flatten(S, F).
flatten([X|S], [X|T]) :- \+(X = []), \+(X = [_|_]), flatten(S, T).
flatten([], []).
Loic
  • 591
  • 3
  • 10
  • `?- flatten(Ls0, Ls).` yields: **Out of local stack**. – mat Jan 17 '17 at 11:13
  • Variables are not allowed in the first argument, indeed. Otherwise, I believe that the correct solution is computed if it exists. – Loic Jan 17 '17 at 11:30