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.
- Antes de tudo, Haskell é uma linguagem puramente funcional, então você garante uma transparência referencial (se não mexer com a
unsafePerformIO
& co.). Isso significa que a avaliação de qualquer expressão, por exemplo f 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 (assumindo f
e x
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:
map f . map g = map (f.g)
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.
- Haskell é uma linguagem preguiçosa . Isso significa que qualquer expressão específica não precisa ser avaliada, a menos que seu resultado seja realmente necessário, e isso também poderia nunca ser. A preguiça é um recurso às vezes debatido e algumas outras linguagens funcionais evitam ou limitam sua aceitação, mas no contexto de Haskell, é um recurso essencial na maneira como a linguagem é usada e projetada. A preguiça é outra ferramenta poderosa nas mãos do otimizador do compilador e, o mais importante, permite que o código seja composto facilmente.
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 e consumer :: [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 chamar consumer
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 inteiro producer
antes de entregá-lo consumer
. De fato, assim que consumer
precisar o primeiro elemento de sua lista de entrada, o código correspondente producer
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ário consumer
, ele não será computado, economizando efetivamente cálculos inúteis. A execução consumer
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, como getSolution
e getAllSolutions
. Em vez disso, esta biblioteca possui apenas uma função solve
, com o seguinte tipo:
solve :: MonadPlus m => SatSolver -> m SatSolver
Aqui, o resultado é um SatSolver
valor agrupado dentro de uma mônada de tipo não especificado, que, no entanto, é restrito a implementar a MonadPlus
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 em SatSolver
valores retornam seus resultados agrupados em uma MonadPlus
instância. Então, suponha que você tenha a fórmula p || !q
e deseje resolvê-la restringindo os resultados que são q
verdadeiros; o uso é o seguinte (as variáveis são numeradas em vez de serem identificadas pelo nome):
expr = Var 1 :||: Not (Var 2)
task :: MonadPlus m => m SatSolver
task = do
pure newSatSolver
assertTrue expr
assertTrue (Var 2)
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 uma Show
instância para SatSolver
, que não existe, mas perdoe-me este ponto).
main = sequence . map print . solve task
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á usar solve
como se retornasse um Maybe SatSolver
valor:
main = case solve task of
Nothing -> putStrLn "No solution"
Just result -> print result
Agora, suponha que você tem duas tarefas de modo integrado, task
e task2
, 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:
combinedTask = task <|> task2
onde <|>
é uma operação binária fornecida pela Alternative
classe de tipo, que é uma super classe de MonadPlus
. 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 a Alternative
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.