Sabemos que no Prolog - predicados não determinísticos são um recurso usado para diminuir problemas combinatórios .
Em Haskell, vemos um comportamento não determinístico semelhante ao Prolog na lista Mônada .
Em Haskell, também vemos o não determinismo na escolha da ordem de avaliação thunk :
Mas não há nada que diga ao GHC qual desses thunks deve ser avaliado primeiro e, portanto, o GHC tem total liberdade para escolher o que avaliar primeiro.
Isso é fascinante - e um tanto libertador. Estou me perguntando (além de lidar com problemas lógicos como oito rainhas ) que princípio isso serve. Havia alguma grande ideia ou grande problema que eles estavam tentando resolver com o não-determinismo?
Minha pergunta é: Qual é o raciocínio por trás de tornar o não determinismo uma característica de Haskell?
fonte
Respostas:
Embora seja verdade que ambos os aspectos citados nas perguntas aparecem como formas de não-determinismo, eles são realmente muito diferentes tanto na forma como trabalham quanto em seus objetivos. Portanto, qualquer resposta deve ser necessariamente dividida em duas partes.
Ordem de avaliação
Haskell não exige nenhuma ordem de execução específica na avaliação de thunks, essencialmente por dois motivos.
unsafePerformIO
& co.). Isso significa que a avaliação de qualquer expressão, por exemplof x
, resultará no mesmo resultado, não importa quantas vezes ela seja avaliada e não importa em que parte do programa ela será avaliada (assumindof
ex
vinculando os mesmos valores nos escopos considerados, de curso). Portanto, ordenar qualquer ordem específica de execução não teria nenhum objetivo , porque sua alteração não produziria efeitos observáveis no resultado do programa. Nesse sentido, essa não é realmente uma forma de não-determinismo, pelo menos nenhuma forma de observação primeiro, uma vez que as diferentes execuções possíveis do programa são todas semanticamente equivalentes.Entretanto, alterar a ordem de execução pode afetar o desempenho do programa, e deixar ao compilador a liberdade de manipular a ordem à sua vontade é fundamental para obter o desempenho incrível que um compilador como o GHC pode obter ao compilar um desempenho tão alto. linguagem de nível. Como exemplo, pense em uma transformação clássica de fusão de fluxo:
Essa igualdade significa simplesmente que aplicar duas funções a uma lista com
map
o mesmo resultado do que aplicar uma vez a composição das duas funções. Isso só é verdade por causa da transparência referencial e é um tipo de transformação que o compilador sempre podeaplicar, não importa o quê. Se alterar a ordem de execução das três funções tivesse efeitos no resultado da expressão, isso não seria possível. Por outro lado, compilá-lo na segunda forma, em vez da primeira, pode ter um enorme impacto no desempenho, porque evita a construção de uma lista temporária e a percorre apenas uma vez. O fato de o GHC poder aplicar automaticamente essa transformação é uma conseqüência direta da transparência referencial e da ordem de execução não fixa e é um dos aspectos principais do excelente desempenho que Haskell pode alcançar.Para entender o que quero dizer com facilidade de composição, considere um exemplo quando você tem uma função
producer :: Int -> [Int]
que executa alguma tarefa complexa para calcular uma lista de algum tipo de dados a partir de um argumento de entrada econsumer :: [Int] -> Int
que é outra função complexa que calcula um resultado de uma lista de dados de entrada. Você as escreveu separadamente, as testou, as otimizou com muito cuidado e as usou isoladamente em diferentes projetos. Agora, no próximo projeto, acontece que você precisa chamarconsumer
o resultado deproducer
. Em uma linguagem não preguiçosa, isso pode não ser o ideal, pois pode ser o caso de a tarefa combinada ser implementada com mais eficiência sem criar uma estrutura de lista temporária. Para obter uma implementação otimizada, você teria que reimplementar a tarefa combinada do zero, testá-la novamente e otimizar novamente.Em haskell, isso não é necessário, e chamar a composição das duas funções
consumer . producer
é perfeitamente adequado. O motivo é que o programa não precisa produzir o resultado inteiroproducer
antes de entregá-loconsumer
. De fato, assim queconsumer
precisar o primeiro elemento de sua lista de entrada, o código correspondenteproducer
será executado o quanto for necessário para produzi-lo, e não mais. Quando o segundo elemento for necessário, ele será calculado. Se algum elemento não for necessárioconsumer
, ele não será computado, economizando efetivamente cálculos inúteis. A execuçãoconsumer
eproducer
será efetivamente intercalado, não apenas evitando o uso de memória da estrutura da lista intermediária, mas também possivelmente evitando cálculos inúteis, e a execução provavelmente será semelhante à versão combinada escrita à mão que você teria que escrever para si mesmo. Isto é o que eu quis dizer com composição . Você tinha dois trechos de código bem testados e com bom desempenho e poderia compor-los, obtendo gratuitamente um trecho de código bem testado e com bom desempenho.Mônadas não determinísticas
O uso de comportamento não determinístico fornecido pelas mônadas da Lista e similares é totalmente diferente. Aqui, o ponto não é o de fornecer ao compilador meios de otimizar seu programa, mas expressar de forma clara e concisa os cálculos que são inerentemente não determinísticos.
Um exemplo do que quero dizer é fornecido pela interface da
Data.Boolean.SatSolver
biblioteca. Ele fornece um solucionador DPLL SAT muito simples implementado em Haskell. Como você deve saber, resolver o problema SAT envolve encontrar uma atribuição de variáveis booleanas que satisfaçam uma fórmula booleana. Entretanto, pode haver mais de uma dessas atribuições, e pode ser necessário encontrá-las ou iterar sobre todas elas, dependendo do aplicativo. Portanto, muitas bibliotecas terão duas funções diferentes, comogetSolution
egetAllSolutions
. Em vez disso, esta biblioteca possui apenas uma funçãosolve
, com o seguinte tipo:Aqui, o resultado é um
SatSolver
valor agrupado dentro de uma mônada de tipo não especificado, que, no entanto, é restrito a implementar aMonadPlus
classe de tipo. Essa classe de tipo é aquela que representa o tipo de não determinismo fornecido pela mônada da lista e, de fato, as listas são instâncias. Todas as funções que operam emSatSolver
valores retornam seus resultados agrupados em umaMonadPlus
instância. Então, suponha que você tenha a fórmulap || !q
e deseje resolvê-la restringindo os resultados que sãoq
verdadeiros; o uso é o seguinte (as variáveis são numeradas em vez de serem identificadas pelo nome):Observe como a instância de mônada e a notação de máscara mascaram todos os detalhes de baixo nível de como as funções gerenciam a
SatSolver
estrutura de dados e nos permitem expressar claramente nossa intenção.Agora, se você deseja obter todos os resultados, basta usar
solve
em um contexto em que o resultado deve ser uma lista. A seguir, imprimiremos todos os resultados na tela (assumindo umaShow
instância paraSatSolver
, que não existe, mas perdoe-me este ponto).No entanto, as listas não são as únicas instâncias de
MonadPlus
.Maybe
é outra instância. Portanto, se você precisar apenas de uma solução, não importa qual, poderá usarsolve
como se retornasse umMaybe SatSolver
valor:Agora, suponha que você tem duas tarefas de modo integrado,
task
etask2
, e você deseja obter uma solução para qualquer um. Mais uma vez, tudo se reúne para que possamos compor nossos blocos de construção pré-existentes:onde
<|>
é uma operação binária fornecida pelaAlternative
classe de tipo, que é uma super classe deMonadPlus
. Novamente, vamos expressar claramente nossa intenção, reutilizando o código sem alterações. O não-determinismo é claramente expresso em código, não oculto sob os detalhes de como o não-determinismo é realmente implementado. Sugiro que você dê uma olhada nos combinadores criados sobre aAlternative
classe type para obter mais exemplos.As mônadas não determinísticas, como listas, não são apenas uma maneira de expressar bons exercícios, mas oferecem uma maneira de projetar códigos elegantes e reutilizáveis que expressam claramente a intenção na implementação de tarefas que são inerentemente não determinísticas.
fonte
task
implementação esteja correta.assertTrue
requer dois parâmetros e você está apenas dando um. Você ainda precisa de um canal um pouco mais explícito doSatSolver
valor entre as funções se quiser usar ado
notação.