Alternativa a la pureza del lenguaje

7

Pureza

Uno de los conceptos interesantes en Haskell es la pureza. Sin embargo, me pregunto cuáles son las razones pragmáticas detrás de esto: permítanme explicar un poco más antes de que rechace mi pregunta.

Mi punto principal es que vamos a tener efectos secundarios sin importar cómo podamos resumirlos en un idioma. Entonces, incluso si un lenguaje de programación como Haskell es puro, solo estamos "posponiendo" los efectos secundarios hasta un punto posterior.

Comprendo la idea de que la pureza facilita el razonamiento sobre los programas (transparencia referencial, etc.). En Haskell, encapsulamos todos los efectos secundarios en el sistema de tipos (la mónada IO). Como tal, no podemos tener un efecto secundario sin propagar la mónada IO a través de los tipos de todas las funciones que llaman a nuestra función impura, por así decirlo.

Como tal, estoy muy interesado en la idea de separar las funciones puras de las impuras en el sistema de tipos. Esta parece ser una buena manera de obligarnos a limitar tales efectos.

Sin embargo, Haskell a menudo es golpeado debido a su naturaleza pura: la gente dice que no es práctico o pragmático. Si esto es cierto o no es otra cuestión, pero el hecho es que muchas personas rechazan a Haskell por este motivo.

¿Un enfoque alternativo? (llamémoslo "contaminación tipo")

A mi parecer, el principal punto ganador no es la pureza en sí misma, sino la separación de código puro e impuro. Si encapsular IO en una mónada es poco práctico, ¿por qué no intentamos un enfoque más simple?

¿Qué pasa si en cambio etiquetamos cada tipo de retorno y tipo de argumento en el sistema como puro o impuro? El lenguaje D tiene una palabra clave "pura" explícita, pero creo que dejamos que las funciones sean puras de forma predeterminada y solo marque las funciones impuras.

En un lenguaje similar a ML, esto podría verse así:

let square x = x * x

let impure getDbData connectionString = ...

let impure getMappedDbData mapping =
    let dbData = getDbData "<some connection info>"
    in map mapping dbData

let main =  // Compiler error, as it is not marked as impure
    let mappedData = getMappedData square
    in ...

Además, llamar a cualquier función impura "contaminaría" el tipo de retorno de las funciones de llamada, convirtiéndolo en impuro. Si no estuviera etiquetado como tal, daría un error de compilación.

(por supuesto, las funciones de paso como argumentos también deberían verificarse con la pureza esperada, pero debería ser fácil inferir la "pureza" esperada de los argumentos)

(la etiqueta "impura" también podría inferirse, supongo que es mejor mantener esta explícita, para facilitar la lectura)

Las funciones puras, por otro lado, se pueden pasar a cualquier parte, sin importar la pureza esperada. Podríamos dar una función pura como entrada a una función que acepta una impura. Por lo tanto, no es una relación simétrica, por lo tanto, la palabra "contaminación".

En general, esto separaría explícitamente (y verificará estáticamente) el código base en una parte pura e impura. En muchos sentidos, está cerca de la funcionalidad de una mónada IO (aunque la asimetría probablemente descarta un mapeo 1 a 1 entre los dos), pero con un desenvolvimiento implícito que se realiza automáticamente. Lo más importante es que muchos programadores lo encontrarán más intuitivo y pragmático que una mónada IO explícita o algo similar.

Y a diferencia de las soluciones completamente impuras, esto da mucha más seguridad para razonar acerca de las partes puras (sin interponerse demasiado en nuestro camino, al definir las impuras). Y en general, es sobre todo una versión aplicada estáticamente de lo que la mayoría de los programadores funcionales consideran la mejor práctica de todos modos (la separación).

TL; DR;

¿Se ha probado alguna vez algo como el enfoque descrito anteriormente? Si no, ¿hay alguna razón por la que no (las razones técnicas, prácticas, teóricas y filosóficas son interesantes)?

Y, finalmente, ¿sería más fácil de aceptar tal control de pureza para las personas que afirman que Haskell no es práctico?

Descargo de responsabilidad

Personalmente no guardo rencor hacia Haskell y la opción de usar mónadas para IO. De hecho, me parece que Haskell es uno de los idiomas más elegantes en uso, y quizás el idioma que más disfruto.

Además, está claro que el enfoque mencionado anteriormente no ofrece algo que la mónada IO no ofrece, lo único es que se parece más (visualmente) a lenguajes impuros como OCaml, F #, etc., que está vinculado a Facilita su comprensión para muchas personas. De hecho, tomar el código F # existente y agregar algunas etiquetas "impuras" sería la única diferencia visual en el código.

Por lo tanto, este podría ser un paso fácil hacia la pureza de los idiomas que no tienen el mismo ecosistema que rodea a las mónadas que tiene Haskell.

    
pregunta nilu 03.10.2014 - 20:45

2 respuestas

14

Ha descrito un sistema de efectos . Es cierto que existen otros sistemas de efectos además de las mónadas, pero en la práctica las mónadas le brindan una gran cantidad de poder expresivo que necesitaría para reinventar en cualquier sistema de efectos prácticos que pueda diseñar.

Por ejemplo:

  • Comienzo etiquetando mis procedimientos de E / S con un efecto io .
  • Mis funciones puras todavía pueden lanzar excepciones, pero lanzar excepciones no es io , así que agrego un efecto exn . Ahora necesito poder combinar efectos.
  • Quiero tener funciones que no sean de E / S que usen la mutación internamente en un montón local h , y quiero que el compilador se asegure de que mis referencias mutables no se escapen de su alcance, así que agrego un st<h> de efecto. Ahora necesito efectos parametrizados.
  • Para las funciones de orden superior, necesito escribir varias versiones para cada combinación de efectos que quiero admitir, por ejemplo, map con una función pura versus map con una función impura. Ahora necesito efecto polimorfismo.
  • Observo que mi tipo definido por el usuario podría usarse para modelar efectos (falla benigna, no determinismo) que el idioma no tiene de forma nativa. Ahora necesito efectos definidos por el usuario.

Las mónadas admiten todos estos casos de uso:

  • Las funciones que pueden devolver errores pueden usar el Either monad.
  • Los efectos se pueden combinar con transformadores de mónada; si necesito excepciones y E / S, puedo usar la mónada EitherT IO .
  • Las mónadas se pueden parametrizar como cualquier otro tipo; ST h monad le permite realizar una mutación local en STRef s en el alcance de un montón h .
  • Debido a la clase de clase Monad , puedo sobrecargar una operación para trabajar en cualquier tipo de efecto. Por ejemplo, mapM :: (Monad m) => (a -> m b) -> [a] -> m [b] funciona en IO , pero también funciona en Either o ST o cualquier otra mónada.
  • Todas las mónadas se implementan como tipos definidos por el usuario, excepto para cosas como IO y ST que deben implementarse en el tiempo de ejecución de Haskell.

Por supuesto, hay algunas verrugas significativas en el uso de las mónadas de Haskell para efectos secundarios.

Por un lado, los transformadores de mónada generalmente no son conmutativos, por lo que StateT Maybe es diferente de MaybeT State , y debes tener cuidado de elegir la combinación que realmente quieres decir.

El mayor problema es que una función puede ser polimórfica en la que se utiliza la mónada, pero no puede ser polimórfica en si se está utilizando una mónada. Entonces obtenemos cargas de código duplicado: map vs. mapM ; zipWith vs. zipWithM , & c.

Ahora, en respuesta a tu pregunta real ...

Koka de Microsoft Research es un lenguaje con un sistema de efectos que no sufre estos problemas . Por ejemplo, el tipo de función map de Koka incluye un parámetro de tipo e para efectos:

map : (xs : list<a>, f : (a) -> e b) -> e list<b>

Y este parámetro se puede crear una instancia del efecto nulo, haciendo que map funcione para funciones puras e impuras por igual. Además, el orden en el que se especifican los efectos es inmaterial: <exn,io> es el mismo que <io,exn> .

También vale la pena mencionar que el mecanismo de excepción comprobada de Java es un ejemplo de un sistema de efectos que la gente ha aceptado ampliamente, pero no creo que responda adecuadamente a tu pregunta porque no es general.

    
respondido por el Jon Purdy 04.10.2014 - 01:42
10

¡Creo que has reinventado las mónadas! Veamos lo que tenemos aquí, podemos "ensuciar" un cálculo puro implícitamente y usarlo en un impuro, y podemos llamar a la función impura desde otro impuro.

Esto se parece mucho a las mónadas, podemos ensuciar un valor puro con return , para llamar a otra función desde una función impura, solo podemos usar >>=

apply :: (a -> IO b) -> IO a -> IO b
apply f a = a >>= f

Ahora su enfoque es un poco más familiar para las personas de otros idiomas: todos tenemos algún tipo de control de pureza en nuestras cabezas para tratar de averiguar qué está sucediendo dónde.

La desventaja es que al hacer de estas cosas un concepto de nivel de idioma, hemos hecho que sea muy difícil desarrollarlo. En Haskell, hay una biblioteca completa de funciones normales definidas por el usuario que funcionan en todas las mónadas. Si incorporáramos IO wholesale al lenguaje, nos veríamos obligados a hacer que todas estas funciones sean primitivas.

Al minimizar la cantidad de magia oscura en torno a la forma en que se maneja la pureza, podemos hacer que sea más fácil trabajar con ellos desde el país del usuario.

    
respondido por el jozefg 03.10.2014 - 22:20