titulo que poner etiqueta ejemplos cursiva como haskell arrows

haskell - poner - que es tags h1 y cursiva



¿Qué son las flechas y cómo puedo usarlas? (5)

Traté de aprender el significado de las flechas , pero no las entendí.

Usé el tutorial de Wikilibros. Creo que el problema de Wikilibro es principalmente que parece estar escrito para alguien que ya entiende el tema.

¿Alguien puede explicar qué son las flechas y cómo puedo usarlas?


Cuando comencé a explorar las composiciones de Arrow (esencialmente Monads), mi enfoque fue romper con la sintaxis funcional y la composición con la que más comúnmente se asocia y comenzar por comprender sus principios utilizando un enfoque más declarativo. Con esto en mente, el siguiente desglose me parece más intuitivo:

function(x) { func1result = func1(x) if(func1result == null) { return null } else { func2result = func2(func1result) if(func2result == null) { return null } else { func3(func2result) }

Entonces, esencialmente, para algún valor x , primero llamamos a una función que suponemos que puede devolver null (func1), otra que puede retornar null o puede asignarse a null intercambiable, finalmente, una tercera función que también puede devolver null . Ahora, dado el valor x , pase x a func3, solo entonces, si no devuelve null , pase este valor a func2, y solo si este valor no es nulo, pase este valor a func1. Es más determinista y el flujo de control le permite construir un manejo de excepciones más sofisticado.

Aquí podemos utilizar la composición de flecha: (func3 <=< func2 <=< func1) x .


De un vistazo a su historial en , voy a suponer que se siente cómodo con algunas de las otras clases de tipos estándar, particularmente Functor y Monoid , y comenzaré con una breve analogía de esas.

La única operación en Functor es fmap , que sirve como una versión generalizada de un map en listas. Este es prácticamente todo el propósito de la clase de tipo; define "cosas sobre las que puedes mapear". Entonces, en cierto sentido Functor representa una generalización de ese aspecto particular de las listas.

Las operaciones para Monoid son versiones generalizadas de la lista vacía y (++) , y define "cosas que se pueden combinar de forma asociativa, con una cosa particular que es un valor de identidad". Las listas son más o menos lo más simple que se ajusta a esa descripción, y Monoid representa una generalización de ese aspecto de las listas.

De la misma manera que las dos anteriores, las operaciones en la clase de categoría Category son versiones generalizadas de id y (.) Y define "cosas que conectan dos tipos en una dirección particular, que pueden conectarse de la cabeza a la cola". Entonces, esto representa una generalización de ese aspecto de las funciones . Notablemente, no se incluyen en la generalización currying o aplicación de función.

La clase de tipo Arrow crea fuera de Category , pero el concepto subyacente es el mismo: las Arrow son elementos que componen funciones similares y tienen una "flecha de identidad" definida para cualquier tipo. Las operaciones adicionales definidas en la clase Arrow solo definen una forma de elevar una función arbitraria a una Arrow y una forma de combinar dos flechas "en paralelo" como una sola flecha entre tuplas.

Por lo tanto, lo primero que hay que tener en cuenta aquí es que las expresiones que crean Arrow son, esencialmente, una composición de funciones elaborada . Los combinadores como (***) y (>>>) son para escribir estilo "pointfree", mientras que la notación de proc ofrece una forma de asignar nombres temporales a las entradas y salidas mientras se conectan las cosas.

Una cosa útil a tener en cuenta aquí es que, aunque Arrow s a veces se describe como "el próximo paso" de Monad s, en realidad no hay una relación terriblemente significativa allí. Para cualquier Monad , puedes trabajar con flechas Kleisli, que son solo funciones con un tipo como a -> mb . El operador (<=<) en Control.Monad es la composición de flecha para estos. Por otro lado, las Arrow no te dan una Monad menos que también incluyas la clase ArrowApply . Entonces no hay una conexión directa como tal.

La diferencia clave aquí es que mientras que las Monad s se pueden usar para secuenciar cálculos y hacer cosas paso a paso, las Arrow son, en cierto sentido, "intemporales", al igual que las funciones normales. Pueden incluir maquinaria adicional y funcionalidad que se empalma con (.) , Pero es más como construir una tubería, sin acumular acciones.

Las otras clases de tipos relacionadas agregan funcionalidad adicional a una flecha, como ser capaz de combinar flechas con Either bien así como con (,) .

Mi ejemplo favorito de una Arrow son los transductores de transmisión con estado , que se parecen a esto:

data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))

Una flecha StreamTrans convierte un valor de entrada a una salida y una versión "actualizada" de sí mismo; considere las formas en que esto difiere de una Monad estado.

¡Escribir instancias para Arrow y sus clases de tipos relacionadas para el tipo anterior puede ser un buen ejercicio para entender cómo funcionan!

También escribí una respuesta similar anteriormente que puede ser útil.


Hay notas de la conferencia de John Hughes de un taller de AFP (Programación Funcional Avanzada). Tenga en cuenta que se escribieron antes de que se cambiaran las clases Arrow en las bibliotecas Base:

http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf


Me gustaría agregar que las flechas en Haskell son mucho más simples de lo que parecen en base a la literatura. Son simplemente abstracciones de funciones.

Para ver cómo esto es prácticamente útil, considere que tiene muchas funciones que desea componer, algunas de ellas son puras y otras son monádicas. Por ejemplo, f :: a -> b , g :: b -> m1 c , y h :: c -> m2 d .

Conociendo cada uno de los tipos involucrados, pude construir una composición a mano, pero el tipo de salida de la composición tendría que reflejar los tipos de mónada intermedios (en el caso anterior, m1 (m2 d) ). ¿Qué pasaría si solo quisiera tratar las funciones como si solo fueran a a -> b , b -> c , y c -> d ? Es decir, quiero abstraer la presencia de mónadas y razonar solo sobre los tipos subyacentes. Puedo usar flechas para hacer exactamente esto.

Aquí hay una flecha que abstrae la presencia de IO para funciones en la mónada IO, de modo que puedo componerlas con funciones puras sin que el código de composición tenga que saber que IO está involucrado . Comenzamos definiendo un IOArrow para envolver las funciones de IO:

data IOArrow a b = IOArrow { runIOArrow :: a -> IO b } instance Category IOArrow where id = IOArrow return IOArrow f . IOArrow g = IOArrow $ f <=< g instance Arrow IOArrow where arr f = IOArrow $ return . f first (IOArrow f) = IOArrow $ /(a, c) -> do x <- f a return (x, c)

Luego hago algunas funciones simples que quiero componer:

foo :: Int -> String foo = show bar :: String -> IO Int bar = return . read

Y úsalas:

main :: IO () main = do let f = arr (++ "!") . arr foo . IOArrow bar . arr id result <- runIOArrow f "123" putStrLn result

Aquí estoy llamando IOArrow y runIOArrow, pero si estuviera pasando estas flechas en una biblioteca de funciones polimórficas, solo tendrían que aceptar argumentos del tipo "Arrow a => abc". No es necesario que ningún código de la biblioteca tenga en cuenta que se trata de una mónada. Solo el creador y el usuario final de la flecha deben saberlo.

La generalización de IOArrow para que funcione en las funciones de cualquier Monad se denomina "flecha Kleisli", y ya existe una flecha incorporada para hacer eso:

main :: IO () main = do let g = arr (++ "!") . arr foo . Kleisli bar . arr id result <- runKleisli g "123" putStrLn result

Por supuesto, también puede utilizar los operadores de composición de flecha y la sintaxis de proceso para aclarar un poco que las flechas están involucradas:

arrowUser :: Arrow a => a String String -> a String String arrowUser f = proc x -> do y <- f -< x returnA -< y main :: IO () main = do let h = arr (++ "!") <<< arr foo <<< Kleisli bar <<< arr id result <- runKleisli (arrowUser h) "123" putStrLn result

Aquí debería quedar claro que, aunque main sabe que la mónada IO está involucrada, arrowUser no. No habría forma de "ocultar" IO de arrowUser sin flechas, no sin recurrir a unsafePerformIO para volver el valor monádico intermedio a uno puro (y así perder ese contexto para siempre). Por ejemplo:

arrowUser'' :: (String -> String) -> String -> String arrowUser'' f x = f x main'' :: IO () main'' = do let h = (++ "!") . foo . unsafePerformIO . bar . id result = arrowUser'' h "123" putStrLn result

Intente escribir eso sin una forma de unsafePerformIO , y sin que arrowUser'' tenga que lidiar con ningún argumento de tipo arrowUser'' .


No conozco un tutorial, pero creo que es más fácil comprender las flechas si miras algunos ejemplos concretos. El mayor problema que tuve al aprender cómo usar las flechas fue que ninguno de los tutoriales o ejemplos realmente muestran cómo usar las flechas, solo cómo componerlas. Entonces, con eso en mente, aquí está mi mini-tutorial. Examinaré dos flechas diferentes: funciones y una flecha definida por el usuario tipo MyArr .

-- type representing a computation data MyArr b c = MyArr (b -> (c,MyArr b c))

1) Una flecha es un cálculo desde la entrada de un tipo específico a la salida de un tipo especificado. La clase de letra de flecha toma tres argumentos de tipo: el tipo de flecha, el tipo de entrada y el tipo de salida. Mirando el encabezado de instancia para las instancias de flecha encontramos:

instance Arrow (->) b c where instance Arrow MyArr b c where

La flecha (ya sea (->) o MyArr ) es una abstracción de un cálculo.

Para una función b -> c , b es la entrada c es la salida.
Para un MyArr bc , b es la entrada c es la salida.

2) Para ejecutar realmente un cálculo de flecha, utiliza una función específica para su tipo de flecha. Para las funciones, simplemente aplica la función a un argumento. Para otras flechas, debe haber una función separada (como runIdentity , runState , etc. para las mónadas).

-- run a function arrow runF :: (b -> c) -> b -> c runF = id -- run a MyArr arrow, discarding the remaining computation runMyArr :: MyArr b c -> b -> c runMyArr (MyArr step) = fst . step

3) Las flechas se utilizan con frecuencia para procesar una lista de entradas. Para las funciones, estas pueden realizarse en paralelo, pero para algunas flechas, la salida en cualquier paso dado depende de entradas previas (por ejemplo, mantener un total acumulado de entradas).

-- run a function arrow over multiple inputs runFList :: (b -> c) -> [b] -> [c] runFList f = map f -- run a MyArr over multiple inputs. -- Each step of the computation gives the next step to use runMyArrList :: MyArr b c -> [b] -> [c] runMyArrList _ [] = [] runMyArrList (MyArr step) (b:bs) = let (this, step'') = step b in this : runMyArrList step'' bs

Esta es una razón por la cual las flechas son útiles. Proporcionan un modelo de cálculo que puede hacer uso implícito del estado sin exponer nunca ese estado al programador. El programador puede usar cómputos con flechas y combinarlos para crear sistemas sofisticados.

Aquí hay un MyArr que cuenta el número de entradas que ha recibido:

-- count the number of inputs received: count :: MyArr b Int count = count'' 0 where count'' n = MyArr (/_ -> (n+1, count'' (n+1)))

Ahora la función runMyArrList count tomará una longitud de lista n como entrada y devolverá una lista de Ints de 1 a n.

Tenga en cuenta que todavía no hemos utilizado ninguna función de "flecha", es decir, métodos de clase de flecha o funciones escritas en términos de ellos.

4) La mayor parte del código anterior es específico de cada instancia de Arrow [1]. Todo en Control.Arrow (y Control.Category ) se trata de componer flechas para crear nuevas flechas. Si pretendemos que la Categoría es parte de Arrow en lugar de una clase separada:

-- combine two arrows in sequence >>> :: Arrow a => a b c -> a c d -> a b d -- the function arrow instance -- >>> :: (b -> c) -> (c -> d) -> (b -> d) -- this is just flip (.) -- MyArr instance -- >>> :: MyArr b c -> MyArr c d -> MyArr b d

La función >>> toma dos flechas y usa la salida de la primera como entrada para la segunda.

Aquí hay otro operador, comúnmente llamado "fanout":

-- &&& applies two arrows to a single input in parallel &&& :: Arrow a => a b c -> a b c'' -> a b (c,c'') -- function instance type -- &&& :: (b -> c) -> (b -> c'') -> (b -> (c,c'')) -- MyArr instance type -- &&& :: MyArr b c -> MyArr b c'' -> MyArr b (c,c'') -- first and second omitted for brevity, see the accepted answer from KennyTM''s link -- for further details.

Como Control.Arrow proporciona un medio para combinar cálculos, he aquí un ejemplo:

-- function that, given an input n, returns "n+1" and "n*2" calc1 :: Int -> (Int,Int) calc1 = (+1) &&& (*2)

Con frecuencia he encontrado funciones como calc1 útiles en pliegues complicados, o funciones que funcionan con punteros, por ejemplo.

La clase de tipo Monad nos proporciona un medio para combinar cálculos monádicos en un único cálculo monádico nuevo usando la función >>= . De forma similar, la clase Arrow nos proporciona los medios para combinar cálculos con flechas en un único cálculo con flecha nueva usando algunas funciones primitivas ( first , arr y *** , con >>> e id de Control.Category). También similar a Mónadas, la pregunta de "¿Qué hace una flecha?" no puede ser generalmente respondida. Depende de la flecha

Lamentablemente, no conozco muchos ejemplos de instancias de flechas en la naturaleza. Las funciones y FRP parecen ser las aplicaciones más comunes. HXT es el único otro uso significativo que viene a la mente.

[1] Excepto count Es posible escribir una función de conteo que haga lo mismo para cualquier instancia de ArrowLoop .