tutorial programacion opciones inteligencia hacer funcional descargar como caracteristicas basico artificial haskell functional-programming monads referential-transparency

programacion - ¿Haskell es verdaderamente puro(es cualquier lenguaje que se ocupa de la entrada y la salida fuera del sistema)?



haskell tutorial (7)

¿Qué significa razonar sobre los sistemas de computación "fuera de las matemáticas de pizarra"? ¿Qué tipo de razonamiento sería? ¿La cuenta muerta?

Los efectos secundarios y las funciones puras son una cuestión de punto de vista. Si consideramos que una función nominalmente colateral es una función que nos lleva de un estado del mundo a otro, es puro nuevamente.

Podemos hacer que cada función de efecto secundario sea pura al darle un segundo argumento, un mundo, y exigir que nos pase un mundo nuevo cuando esté hecho. Ya no sé nada de C++ pero digo que read tiene una firma como esta:

vector<char> read(filepath_t)

En nuestro nuevo "estilo puro", lo manejamos así:

pair<vector<char>, world_t> read(world_t, filepath_t)

De hecho, así es como funciona cada acción Haskell IO.

Entonces ahora tenemos un modelo puro de IO. Gracias a dios. Si no pudiéramos hacer eso, quizás Lambda Calculus y Turing Machines no sean formalismos equivalentes y entonces tendríamos algo que explicar. Todavía no hemos terminado, pero los dos problemas que nos quedan son fáciles:

  • ¿Qué entra en la estructura world_t ? Una descripción de cada grano de arena, hoja de hierba, corazón roto y puesta de sol dorado?

  • Tenemos una regla informal de que usamos un mundo solo una vez: después de cada operación IO, desechamos el mundo que usamos con él. Sin embargo, con todos estos mundos flotando, estamos destinados a confundirlos.

El primer problema es bastante fácil. Mientras no permitamos la inspección del mundo, resulta que no necesitamos preocuparnos por almacenar nada en él. Solo debemos asegurarnos de que un nuevo mundo no sea igual a cualquier mundo anterior (no sea que el compilador optimice desviadamente algunas operaciones que producen el mundo, como lo hace a veces en C++ ). Hay muchas formas de manejar esto.

En cuanto a los mundos que se mezclan, nos gustaría esconder el mundo que pasa dentro de una biblioteca para que no haya forma de llegar a los mundos y por lo tanto no hay forma de mezclarlos. Resulta que las mónadas son una gran manera de ocultar un "canal lateral" en un cálculo. Ingrese la mónada IO.

Hace algún tiempo, una pregunta como la tuya fue formulada en la lista de correo de Haskell y allí ingresé al "canal lateral" con más detalle. Aquí está el hilo de Reddit (que se vincula a mi correo electrónico original):

http://www.reddit.com/r/haskell/comments/8bhir/why_the_io_monad_isnt_a_dirty_hack/

Después de tocar las Mónadas con respecto a la programación funcional, ¿la característica realmente hace que un lenguaje sea puro, o es simplemente otra "tarjeta gratuita para salir de la cárcel" para razonar los sistemas informáticos en el mundo real, fuera de las matemáticas de pizarra?

EDITAR:

Esto no es cebo de fuego como alguien ha dicho en este post, pero es una pregunta genuina con la que espero que alguien pueda derribarme y decir, prueba, es pura.

También estoy viendo la pregunta con respecto a otros lenguajes funcionales no tan puros y algunos lenguajes OO que usan un buen diseño y comparan la pureza. Hasta ahora, en mi limitado mundo de FP, todavía no he probado la pureza de las Mónadas, les complacerá saber, sin embargo, que me gusta la idea de la inmutabilidad, que es mucho más importante en lo que se refiere a la pureza.


Es importante entender que no hay nada intrínsecamente especial sobre las mónadas, por lo que definitivamente no representan una tarjeta de "salir de la cárcel" a este respecto. No hay magia de compilador (u otra) necesaria para implementar o usar mónadas, se definen en el entorno puramente funcional de Haskell. En particular, sdcvvc ha mostrado cómo definir mónadas de manera puramente funcional, sin ningún recurso para la implementación de puertas traseras.


Lo pienso así: los programas tienen que hacer algo con el mundo exterior para ser útiles. Lo que está sucediendo (o debería estar sucediendo) cuando se escribe el código (en cualquier idioma) es que se esfuerza por escribir la mayor cantidad de código puro, libre de efectos secundarios posible y acorralar el IO en lugares específicos.

Lo que tenemos en Haskell es que te empujan más en esta dirección de escritura para controlar los efectos. En el núcleo y en muchas bibliotecas también hay una enorme cantidad de código puro. Haskell realmente se trata de esto. Las mónadas en Haskell son útiles para muchas cosas. Y una cosa para la que han sido utilizados es la contención del código que trata sobre las impurezas.

Esta forma de diseñar junto con un lenguaje que lo facilita en gran medida tiene un efecto general de ayudarnos a producir un trabajo más confiable, requiriendo menos pruebas unitarias para tener claridad sobre cómo se comporta y permitir una mayor reutilización mediante la composición.

Si entiendo lo que estás diciendo correctamente, no veo esto como algo falso o solo en nuestras mentes, como una "tarjeta para salir de la cárcel". Los beneficios aquí son muy reales.


No, no lo es. La mónada IO es impura porque tiene efectos secundarios y estado variable (las condiciones de carrera son posibles en los programas Haskell, entonces ... eh ... el lenguaje FP puro no sabe algo como "condición de carrera"). La FP realmente pura es Limpia con tipificación de exclusividad, o Elm con FRP (programación reactiva funcional) y no Haskell. Haskell es una gran mentira.

Prueba:

import Control.Concurrent import System.IO as IO import Data.IORef as IOR import Control.Monad.STM import Control.Concurrent.STM.TVar limit = 150000 threadsCount = 50 -- Don''t talk about purity in Haskell when we have race conditions -- in unlocked memory ... PURE language don''t need LOCKING because -- there isn''t any mutable state or another side effects !! main = do hSetBuffering stdout NoBuffering putStr "Lock counter? : " a <- getLine if a == "y" || a == "yes" || a == "Yes" || a == "Y" then withLocking else noLocking noLocking = do counter <- newIORef 0 let doWork = mapM_ (/_ -> IOR.modifyIORef counter (/x -> x + 1)) [1..limit] threads <- mapM (/_ -> forkIO doWork) [1..threadsCount] -- Sorry, it''s dirty but time is expensive ... threadDelay (15 * 1000 * 1000) val <- IOR.readIORef counter IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ " but it is " ++ show val) withLocking = do counter <- atomically (newTVar 0) let doWork = mapM_ (/_ -> atomically $ modifyTVar counter (/x -> x + 1)) [1..limit] threads <- mapM (/_ -> forkIO doWork) [1..threadsCount] threadDelay (15 * 1000 * 1000) val <- atomically $ readTVar counter IO.putStrLn ("It may be " ++ show (threadsCount * limit) ++ " but it is " ++ show val)



Soy muy nuevo en programación funcional, pero así es como lo entiendo:

En haskell, defines un conjunto de funciones. Estas funciones no se ejecutan. Ellos pueden ser evaluados.

Hay una función en particular que se evalúa. Esta es una función constante que produce un conjunto de "acciones". Las acciones incluyen la evaluación de funciones y la realización de IO y otras cosas del "mundo real". Puede tener funciones que creen y transmitan estas acciones y nunca se ejecutarán a menos que una función se evalúe con inseguroPerformIO o la función principal las devuelva.

En resumen, un programa Haskell es una función, compuesta de otras funciones, que devuelve un programa imperativo. El programa Haskell en sí es puro. Obviamente, ese programa imperativo en sí mismo no puede ser. Las computadoras del mundo real son por definición impuras.

Hay mucho más en esta cuestión y mucho de esto es una cuestión de semántica (humana, no de lenguaje de programación). Las mónadas son también un poco más abstractas de lo que he descrito aquí. Pero creo que esta es una forma útil de pensar sobre esto en general.


Tome el siguiente mini-lenguaje:

data Action = Get (Char -> Action) | Put Char Action | End

Get f significa: lea un carácter c y realice la acción fc .

Put ca significa: escribe el carácter c y realiza la acción a .

Aquí hay un programa que imprime "xy", luego pide dos letras y las imprime en orden inverso:

Put ''x'' (Put ''y'' (Get (/a -> Get (/b -> Put b (Put a End)))))

Usted puede manipular tales programas. Por ejemplo:

conditionally p = Get (/a -> if a == ''Y'' then p else End)

Tiene el tipo Action -> Action - toma un programa y le da otro programa que primero solicita confirmación. Aquí está otro:

printString = foldr Put End

Esto tiene tipo String -> Action - toma una cadena y devuelve un programa que escribe la cadena, como

Put ''h'' (Put ''e'' (Put ''l'' (Put ''l'' (Put ''o'' End)))) .

IO en Haskell funciona de manera similar. Aunque ejecutarlo requiere realizar efectos secundarios, puedes crear programas complejos sin ejecutarlos, de una manera pura. Está calculando las descripciones de los programas (acciones IO) y no las está ejecutando.

En un lenguaje como C, puede escribir una función void execute(Action a) que realmente ejecutó el programa. En Haskell especifica esa acción escribiendo main = a . El compilador crea un programa que ejecuta la acción, pero no tiene otra forma de ejecutar una acción (aparte de los trucos sucios).

Obviamente, Get y Put no son solo opciones, puede agregar muchas otras llamadas de API al tipo de datos IO, como operar en archivos o concurrencia.

Agregar un valor de resultado

Ahora considere el siguiente tipo de datos.

data IO a = Get (Char -> Action) | Put Char Action | End a

El tipo de Action anterior es equivalente a IO () , es decir, un valor IO que siempre devuelve "unidad", comparable a "vacío".

Este tipo es muy similar a Haskell IO, solo en Haskell IO es un tipo de datos abstracto (no tiene acceso a la definición, solo a algunos métodos).

Estas son acciones de IO que pueden terminar con algún resultado. Un valor como este:

Get (/x -> if x == ''A'' then Put ''B'' (End 3) else End 4)

tiene tipo IO Int y corresponde a un programa C:

int f() { char x; scanf("%c", &x); if (x == ''A'') { printf("B"); return 3; } else return 4; }

Evaluación y ejecución

Hay una diferencia entre evaluar y ejecutar. Puede evaluar cualquier expresión de Haskell y obtener un valor; por ejemplo, evalúe 2 + 2 :: Int into 4 :: Int. Puede ejecutar expresiones Haskell solo que tengan tipo IO a. Esto podría tener efectos secundarios; ejecutar Put ''a'' (End 3) pone la letra a en la pantalla. Si evalúa un valor de IO, haga lo siguiente:

if 2+2 == 4 then Put ''A'' (End 0) else Put ''B'' (End 2)

usted obtiene:

Put ''A'' (End 0)

Pero no hay efectos secundarios; solo realizó una evaluación, que es inofensiva.

¿Cómo traducirías?

bool comp(char x) { char y; scanf("%c", &y); if (x > y) { //Character comparison printf(">"); return true; } else { printf("<"); return false; } }

en un valor de IO?

Arregle un personaje, diga ''v''. Ahora comp(''v'') es una acción IO, que compara el carácter dado con ''v''. Del mismo modo, comp(''b'') es una acción IO, que compara el carácter dado con ''b''. En general, comp es una función que toma un carácter y devuelve una acción IO.

Como programador en C, podría argumentar que comp(''b'') es un booleano. En C, la evaluación y la ejecución son idénticas (es decir, significan lo mismo o ocurren simultáneamente). No en Haskell. comp(''b'') evalúa en alguna acción IO, que después de ser ejecutada da un booleano. (Precisamente, se evalúa en el bloque de código como se indica arriba, solo con ''b'' sustituido por x)

comp :: Char -> IO Bool comp x = Get (/y -> if x > y then Put ''>'' (End True) else Put ''<'' (End False))

Ahora, comp ''b'' evalúa en Get (/y -> if ''b'' > y then Put ''>'' (End True) else Put ''<'' (End False)) .

También tiene sentido matemáticamente. En C, int f() es una función. Para un matemático, esto no tiene sentido, ¿una función sin argumentos? El objetivo de las funciones es tomar argumentos. Una función int f() debería ser equivalente a int f . No lo es, porque las funciones en C combinan funciones matemáticas y acciones IO.

Primera clase

Estos valores de IO son de primera clase. Al igual que puede tener una lista de listas de tuplas de enteros [[(0,2),(8,3)],[(2,8)]] puede construir valores complejos con IO.

(Get (/x -> Put (toUpper x) (End 0)), Get (/x -> Put (toLower x) (End 0))) :: (IO Int, IO Int)

Una tupla de acciones IO: primero lee un carácter y lo imprime en mayúsculas, segundo lee un carácter y lo devuelve en minúscula.

Get (/x -> End (Put x (End 0))) :: IO (IO Int)

Un valor IO, que lee un carácter x termina, y devuelve un valor IO que escribe x en la pantalla.

Haskell tiene funciones especiales que permiten una fácil manipulación de los valores de IO. Por ejemplo:

sequence :: [IO a] -> IO [a]

que toma una lista de acciones IO y devuelve una acción IO que las ejecuta en secuencia.

Mónadas

Las mónadas son algunos combinators (como conditionally arriba), que te permiten escribir programas de una manera más estructural. Hay una función que se compone de tipo

IO a -> (a -> IO b) -> IO b

que dado IO a, y una función a -> IO b, devuelve un valor de tipo IO b. Si escribe el primer argumento como una función C af() y el segundo argumento como bg(ax) , devuelve un programa para g(f(x)) . Dada la definición anterior de Acción / IO, puede escribir esa función usted mismo.

Tenga en cuenta que las mónadas no son esenciales para la pureza: siempre puede escribir programas como lo hice anteriormente.

Pureza

Lo esencial de la pureza es la transparencia referencial y la distinción entre evaluación y ejecución.

En Haskell, si tienes f x+fx puedes reemplazar eso con 2*fx . En C, f(x)+f(x) en general no es lo mismo que 2*f(x) , ya que f podría imprimir algo en la pantalla, o modificar x .

Gracias a la pureza, un compilador tiene mucha más libertad y puede optimizar mejor. Puede reordenar los cálculos, mientras que en C tiene que pensar si eso cambia el significado del programa.