Existem algumas respostas muito boas. Vou tentar contribuir para a discussão.
Sobre o tema da programação lógica declarativa em Prolog, há o grande livro "The Craft of Prolog", de Richard O'Keefe . Trata-se de escrever programas eficientes usando uma linguagem de programação que permite escrever programas muito ineficientes. Neste livro, ao discutir as implementações eficientes de vários algoritmos (no capítulo "Métodos de programação"), o autor adota a seguinte abordagem:
- defina o problema em inglês
- escreva uma solução funcional que seja o mais declarativa possível; normalmente, isso significa exatamente o que você tem na sua pergunta, corrija o Prolog
- a partir daí, tome medidas para refinar a implementação para torná-la mais rápida
A observação mais esclarecedora (para mim) que pude fazer enquanto trabalhava no caminho:
Sim, a versão final da implementação é muito mais eficiente que a especificação "declarativa" com a qual o autor começou. Ainda é muito declarativo, sucinto e fácil de entender. O que aconteceu no meio é que a solução final captura propriedades do problema ao qual a solução inicial estava alheia.
Em outras palavras, ao implementar uma solução, usamos o máximo de nosso conhecimento sobre o problema possível. Comparar:
Encontre uma permutação de uma lista de forma que todos os elementos estejam em ordem crescente
para:
Mesclar duas listas classificadas resultará em uma lista classificada. Como pode haver sublistas que já estão classificadas, use-as como ponto de partida, em vez de sublistas de comprimento 1.
Um pequeno aparte: uma definição como a que você forneceu é atraente porque é muito geral. No entanto, não posso escapar da sensação de que ela propositalmente ignora o fato de que permutações são, bem, um problema combinatório. Isso é algo que já sabemos ! Isso não é uma crítica, apenas uma observação.
Quanto à verdadeira questão: como avançar? Bem, uma maneira é fornecer o máximo de conhecimento sobre o problema que estamos declarando ao computador.
A melhor tentativa que conheço para realmente resolver o problema é apresentada nos livros de co-autoria de Alexander Stepanov, "Elements of Programming" e "From Mathematics to Generic Programming" . Infelizmente, não estou preparado para resumir (ou mesmo entender completamente) tudo nesses livros. No entanto, a abordagem adotada para definir algoritmos de biblioteca e estruturas de dados eficientes (ou mesmo ótimos), sob a condição de que todas as propriedades relevantes da entrada sejam conhecidas antecipadamente. O resultado final é:
- Cada transformação bem definida é um refinamento das restrições que já existem (as propriedades conhecidas);
- Deixamos o computador decidir qual transformação é ideal com base nas restrições existentes.
Quanto ao porquê ainda não aconteceu, bem, a ciência da computação é um campo muito jovem, e ainda estamos lidando com a verdadeira apreciação da novidade da maior parte dele.
PS
Para dar uma idéia do que quero dizer com "refinar a implementação": considere, por exemplo, o problema fácil de obter o último elemento de uma lista, no Prolog. A solução declarativa canônica é dizer:
last(List, Last) :-
append(_, [Last], List).
Aqui, o significado declarativo de append/3
é:
List1AndList2
é a concatenação List1
eList2
Como no segundo argumento append/3
, temos uma lista com apenas um elemento e o primeiro argumento é ignorado (o sublinhado), obtemos uma divisão da lista original, que descarta a frente da lista ( List1
no contexto de append/3
) e exige que o verso ( List2
no contexto de append/3
) é de fato uma lista com apenas um elemento: portanto, é o último elemento.
A implementação real fornecida pelo SWI-Prolog , no entanto, diz:
last([X|Xs], Last) :-
last_(Xs, X, Last).
last_([], Last, Last).
last_([X|Xs], _, Last) :-
last_(Xs, X, Last).
Isso ainda é muito declarativo. Leia de cima para baixo, diz:
O último elemento de uma lista só faz sentido para uma lista de pelo menos um elemento. O último elemento para um par de cauda e a cabeça de uma lista é: a cabeça, quando a cauda está vazia, ou o último da cauda não vazia.
O motivo pelo qual essa implementação é fornecida é solucionar as questões práticas que envolvem o modelo de execução do Prolog. Idealmente, não deve fazer diferença qual implementação é usada. Da mesma forma, poderíamos ter dito:
last(List, Last) :-
reverse(List, [Last|_]).
O último elemento de uma lista é o primeiro elemento da lista invertida.
Se você deseja obter o preenchimento de discussões inconclusivas sobre o que é bom, o Prolog declarativo, basta passar por algumas das perguntas e respostas na tag Prolog no Stack Overflow .