Sabemos que en Prolog , los predicados no deterministas son una característica utilizada para reducir los problemas combinatorios .
En Haskell vemos un comportamiento no determinista similar al de Prolog en la Lista Mónada .
En Haskell también vemos el no determinismo al elegir el orden de evaluación thunk :
Pero no hay nada que le diga a GHC cuál de esos thunks debe evaluarse primero, y por lo tanto, GHC tiene plena libertad para elegir el thunk que se evalúe primero.
Esto es fascinante, y algo liberador. Me pregunto (aparte de lidiar con problemas lógicos como las ocho reinas ) a qué principio sirve. ¿Hubo alguna gran idea o gran problema que intentaban resolver con el no determinismo?
Mi pregunta es: ¿Cuál es el razonamiento detrás de hacer que el no determinismo sea una característica de Haskell?
fuente
Respuestas:
Si bien es cierto que ambos aspectos citados en las preguntas aparecen como formas de no determinismo, de hecho son bastante diferentes tanto en cómo funcionan como en sus objetivos. Por lo tanto, cualquier respuesta tiene que dividirse necesariamente en dos partes.
Orden de evaluación
Haskell no exige ninguna orden de ejecución particular en la evaluación de thunks esencialmente debido a dos razones.
unsafePerformIO
& co). Esto significa que la evaluación de cualquier expresión, por ejemplo ,f x
dará como resultado el mismo resultado sin importar cuántas veces se evalúe y sin importar en qué parte del programa se evalúe (suponiendof
yx
vinculando los mismos valores en los ámbitos considerados, de curso). Por lo tanto, ordenar cualquier orden particular de ejecución no tendría ningún propósito , porque cambiarlo no produciría ningún efecto observable en el resultado del programa. En este sentido, esto no es realmente una forma de no determinismo, al menos no es una forma observable uno, ya que las diferentes ejecuciones posibles del programa son semánticamente equivalentes.Sin embargo, cambiar el orden de ejecución puede tener un efecto en el rendimiento del programa, y dejar al compilador la libertad de manipular el orden a su voluntad es fundamental para lograr el rendimiento sorprendente que un compilador como GHC puede obtener compilando un nivel tan alto. lenguaje de nivel Como ejemplo, piense en una transformación clásica de fusión de flujo:
Esta igualdad simplemente significa que aplicar dos funciones a una lista
map
tiene el mismo resultado que aplicar una vez la composición de las dos funciones. Esto solo es cierto debido a la transparencia referencial, y es un tipo de transformación que el compilador siempre puedeaplicar, pase lo que pase. Si cambiar el orden de ejecución de las tres funciones tuviera efectos en el resultado de la expresión, esto no sería posible. Por otro lado, compilarlo en la segunda forma en lugar de la primera puede tener un gran impacto en el rendimiento, ya que evita la construcción de una lista temporal y solo atraviesa la lista una vez. El hecho de que GHC pueda aplicar automáticamente dicha transformación es una consecuencia directa de la transparencia referencial y el orden no fijo de ejecución y es uno de los aspectos clave del gran desempeño que Haskell puede lograr.Para ver a qué me refiero con facilidad de composición, considere un ejemplo cuando tiene una función
producer :: Int -> [Int]
que realiza una tarea compleja para calcular una lista de algún tipo de datos a partir de un argumento de entrada, yconsumer :: [Int] -> Int
que es otra función compleja que computa un resultado de una lista de datos de entrada. Los ha escrito por separado, los ha probado, los ha optimizado con mucho cuidado y los ha utilizado de forma aislada en diferentes proyectos. Ahora en el próximo proyecto sucede que tienes que recurrirconsumer
al resultado deproducer
. En un lenguaje no perezoso, esto puede no ser óptimo, ya que puede darse el caso de que la tarea combinada se implemente más eficientemente sin construir una estructura de lista temporal. Para obtener una implementación optimizada, tendría que volver a implementar la tarea combinada desde cero, volver a probarla y volver a optimizarla.En Haskell esto no es necesario, y llamar a la composición de las dos funciones
consumer . producer
está perfectamente bien. La razón es que no se requiere que el programa produzca realmente el resultado completoproducer
antes de entregarloconsumer
. De hecho, tan pronto comoconsumer
necesite el primer elemento de su lista de entrada, el código correspondienteproducer
se ejecutará tanto como sea necesario para producirlo, y no más. Cuando se necesita el segundo elemento, se computará. Si algún elemento no será necesarioconsumer
, no se calculará en absoluto, ahorrando efectivamente cálculos inútiles. La ejecución deconsumer
yproducer
efectivamente se intercalará, no solo evitando el uso de la memoria de la estructura de la lista intermedia, sino también evitando cálculos inútiles, y la ejecución probablemente sería similar a la versión combinada escrita a mano que de lo contrario tendría que escribir usted mismo. Esto es lo que quise decir con composición . Usted tenía dos piezas de código bien probadas y con buen rendimiento y podía componerlas obteniendo de forma gratuita una pieza de código bien probada y con buen rendimiento.Mónadas no deterministas
El uso del comportamiento no determinista provisto por la Lista de mónadas y similares es totalmente diferente. Aquí no se trata de proporcionar al compilador los medios para optimizar su programa, sino de expresar de manera clara y concisa los cálculos que son inherentemente no deterministas.
La interfaz de la
Data.Boolean.SatSolver
biblioteca proporciona un ejemplo de lo que quiero decir . Proporciona un solucionador DPLL SAT muy simple implementado en Haskell. Como ya sabrás, resolver el problema SAT implica encontrar una asignación de variables booleanas que satisfaga una fórmula booleana. Sin embargo, puede haber más de una de esas tareas, y uno puede necesitar encontrar alguna de ellas, o iterar sobre todas ellas, dependiendo de la aplicación. Por lo tanto, muchas bibliotecas tendrán dos funciones diferentes comogetSolution
ygetAllSolutions
. En cambio, esta biblioteca solo tiene una funciónsolve
, con el siguiente tipo:Aquí, el resultado es un
SatSolver
valor envuelto dentro de una mónada de tipo no especificado, que sin embargo está obligado a implementar laMonadPlus
clase de tipo. Esta clase de tipo es la que representa el tipo de no determinismo proporcionado por la mónada de la lista, y de hecho las listas son instancias. Todas las funciones que operan enSatSolver
valores devuelven sus resultados en unaMonadPlus
instancia. Entonces, suponga que tiene la fórmulap || !q
y desea resolverla restringiendo los resultados que establecenq
verdadero, entonces el uso es el siguiente (las variables se numeran en lugar de identificarse por nombre):Observe cómo la instancia de mónada y la notación do ocultan todos los detalles de bajo nivel de cómo las funciones manejan la
SatSolver
estructura de datos, y nos permiten expresar claramente nuestra intención.Ahora, si desea obtener todos los resultados, simplemente use
solve
en un contexto donde su resultado tiene que ser una lista. Lo siguiente imprimirá todos los resultados en pantalla (suponiendo unaShow
instancia paraSatSolver
, que no existe, pero perdóname este punto).Sin embargo, las listas no son las únicas instancias de
MonadPlus
.Maybe
Es otra instancia. Entonces, si solo necesita una solución, sin importar cuál, puede usarsolve
como si devolviera unMaybe SatSolver
valor:Ahora supongamos que tiene dos tareas por lo construyeron,
task
ytask2
, y desea obtener una solución a cualquiera de ellos. Una vez más, todo se une para permitirnos componer nuestros bloques de construcción preexistentes:donde
<|>
es una operación binaria proporcionada por laAlternative
clase de tipos, que es una superclase deMonadPlus
. Esto nuevamente nos permite expresar claramente nuestra intención, reutilizando el código sin cambios. El no determinismo se expresa claramente en código, no se oculta bajo los detalles de cómo se implementa realmente el no determinismo. Le sugiero que eche un vistazo a los combinadores integrados en la parte superior de laAlternative
clase de tipos para obtener más ejemplos.Las mónadas no deterministas, como las listas, no son solo una forma de expresar buenos ejercicios, sino que ofrecen una forma de diseñar un código elegante y reutilizable que exprese claramente la intención en la implementación de tareas que son inherentemente no deterministas.
fuente
task
implementación sea correcta.assertTrue
toma dos parámetros, y solo estás dando uno. Todavía necesitará un poco más de canalización explícita delSatSolver
valor entre las funciones si va a usar lado
notación.