A lot of these answers use last or append, which are expensive operations.
You can do half of a reverse and then check equality.
The question stipulated not to use reverse, but this only uses a reverse like process, not a full reverse.
palindrome(Xs):- palindrome(Xs,[]).
palindrome(Xs, Xs). % [1,2,2,1] will get to pal([1,2],[1,2])
palindrome([X|Xs],Xs). % captures a case like [a,b,c,b,a]
palindrome([X|Xs],Ys):- palindrome(Xs, [X|Ys]). % reverse-like process
One thing possibly missing from the above is cuts. Although no necessary here, should be used for good practice:
palindrome(Xs):- palindrome(Xs,[]).
palindrome(Xs, Xs):- !. % Don't need to redo after positive match
palindrome([X|Xs],Xs):- !.
palindrome([X|Xs],Ys):- palindrome(Xs, [X|Ys]).
Typical trace for a palindrome:
[trace] 88 ?- pal([1,2,1]).
Call: (6) pal([1, 2, 1]) ? creep
Call: (7) pal([1, 2, 1], []) ? creep
Call: (8) pal([2, 1], [1]) ? creep
Exit: (8) pal([2, 1], [1]) ? creep % Matches rule - palindrome([X|Xs],Xs).
Exit: (7) pal([1, 2, 1], []) ? creep
Exit: (6) pal([1, 2, 1]) ? creep
true .
And a non-palindrome:
[trace] 87 ?- pal([1,2,3]).
Call: (6) pal([1, 2, 3]) ? creep
Call: (7) pal([1, 2, 3], []) ? creep
Call: (8) pal([2, 3], [1]) ? creep
Call: (9) pal([3], [2, 1]) ? creep
Call: (10) pal([], [3, 2, 1]) ? creep
Fail: (10) pal([], [3, 2, 1]) ? creep % Fails as [] doesn't equal [3,2,1] and can't be pulled apart
Fail: (9) pal([3], [2, 1]) ? creep
Fail: (8) pal([2, 3], [1]) ? creep
Fail: (7) pal([1, 2, 3], []) ? creep
Fail: (6) pal([1, 2, 3]) ? creep
false.