¿Cuál es el propósito de las flechas?

63

Estoy aprendiendo programación funcional con Haskell, y trato de captar conceptos entendiendo primero por qué los necesito.

Me gustaría saber el objetivo de las flechas en los lenguajes de programación funcionales. ¿Qué problema resuelven? Revisé http://en.wikibooks.org/wiki/Haskell/Understanding_arrows y http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf . Todo lo que entiendo es que se usan para describir gráficos para cálculos, y que permiten una codificación de estilo libre de puntos más fácil.

El artículo supone que el estilo libre de puntos es generalmente más fácil de entender y escribir. Esto me parece bastante subjetivo. En otro artículo ( http://en.wikibooks.org/wiki/Haskell/StephensArrowTutorial#Hangman:_Main_program ), se implementa un juego del ahorcado, pero no puedo ver cómo las flechas hacen que esta implementación sea natural.

Pude encontrar muchos documentos que describen el concepto, pero nada sobre la motivación.

¿Qué me estoy perdiendo?

Simon Bergot
fuente

Respuestas:

43

Me doy cuenta de que llego tarde a la fiesta, pero has tenido dos respuestas teóricas aquí, y quería ofrecerte una alternativa práctica para analizar. Estoy llegando a esto como un novato relativo de Haskell que, sin embargo, recientemente ha sido forzado a marchar a través del tema de Arrows para un proyecto en el que estoy trabajando actualmente.

Primero, puede resolver productivamente la mayoría de los problemas en Haskell sin alcanzar las Flechas. Algunos Haskellers notables realmente no les gustan y no los usan (vea aquí , aquí y aquí para más información sobre esto). Entonces, si te estás diciendo a ti mismo "Oye, no necesito esto", comprende que realmente puedes estar en lo correcto.

Lo que encontré más frustrante acerca de Arrows cuando los aprendí por primera vez fue cómo los tutoriales sobre el tema inevitablemente alcanzaron la analogía de los circuitos. Si observa el código Arrow, la variedad azucarada, al menos, no se parece en nada a un lenguaje de definición de hardware. Sus entradas se alinean a la derecha, sus salidas a la izquierda, y si no las conecta correctamente, simplemente no se dispararán. Pensé para mí mismo: ¿en serio? ¿Es aquí donde hemos terminado? ¿Hemos creado un lenguaje tan completamente de alto nivel que una vez más consiste en cables de cobre y soldadura?

La respuesta correcta a esto, por lo que he podido determinar, es: en realidad, sí. El caso de uso asesino en este momento para Arrows es FRP (piense en Yampa, juegos, música y sistemas reactivos en general). El problema que enfrenta FRP es en gran medida el mismo problema que enfrentan todos los demás sistemas de mensajería síncrona: cómo conectar un flujo continuo de entradas a un flujo continuo de salidas sin dejar caer información relevante o filtraciones. Puede modelar las secuencias como listas (varios sistemas FRP recientes usan este enfoque), pero cuando tiene muchas entradas, las listas se vuelven casi imposibles de administrar. Necesita aislarse de la corriente.

Lo que permiten las flechas en los sistemas FRP es la composición de funciones en una red, mientras que al mismo tiempo abstrae por completo cualquier referencia a los valores subyacentes que esas funciones transmiten. Si eres nuevo en FP, esto puede ser confuso al principio, y luego alucinante cuando has absorbido las implicaciones de esto. Recientemente ha absorbido la idea de que las funciones pueden abstraerse y cómo entender que una lista [(*), (+), (-)]es de tipo [(a -> a -> a)]. Con las flechas, puede impulsar la abstracción una capa más.

Esta capacidad adicional de abstracción conlleva sus propios peligros. Por un lado, puede empujar a GHC a casos de esquina donde no sabe qué hacer con sus supuestos de tipo. Tendrá que estar preparado para pensar a nivel de tipo: esta es una excelente oportunidad para aprender sobre tipos y RankNTypes y otros temas similares.

También hay una serie de ejemplos de lo que yo llamaría "Stupid Arrow Stunts" donde el codificador busca un combinador de Arrow solo porque quiere mostrar un buen truco con tuplas. (Aquí está mi propia contribución trivial a la locura ). Siéntase libre de ignorar tales perritos calientes cuando lo encuentre en la naturaleza.

NOTA: Como mencioné anteriormente, soy un novato relativo. Si he promulgado alguna idea errónea anterior, no dude en corregirme.

rtperson
fuente
2
Estoy feliz de no haber aceptado nada todavía. Gracias por brindar esta respuesta. Está más enfocado en los usuarios. Los ejemplos son geniales. Las partes subjetivas están claramente definidas y equilibradas. Espero que las personas que votaron por esta pregunta regresen y vean esto.
Simon Bergot
Si bien las flechas son definitivamente la herramienta incorrecta para su solución vinculada, creo que debo mencionar que removeAt' n = arr(\ xs -> (xs,xs)) >>> arr (take (n-1)) *** arr (drop n) >>> arr (uncurry (++)) >>> returnAse puede escribir de manera más concisa y clara removeAt' n = (arr (take $ n-1) &&& arr (drop n)) >>> (arr $ uncurry (++)).
cemper93
30

Esta es una especie de respuesta "suave", y no estoy seguro de si alguna referencia realmente lo dice de esta manera, pero así es como he llegado a pensar en las flechas:

Un tipo de flecha A b ces básicamente una función, b -> cpero con más estructura de la misma manera que un valor monádico M atiene más estructura que un antiguo simple a.

Ahora, cuál es esa estructura adicional depende de la instancia de flecha particular de la que estás hablando. Al igual que con las mónadas IO ay Maybe acada una tiene una estructura adicional diferente.

Lo que obtienes con las mónadas es la incapacidad de ir de un M aa un a. Ahora, esto puede parecer una limitación, pero en realidad es una característica: el sistema de tipos lo protege de convertir un valor monádico en un valor antiguo simple. Solo puede hacer uso del valor participando en la mónada vía >>=o en las operaciones primitivas de la instancia de mónada particular.

Del mismo modo, lo que se obtiene A b ces la incapacidad de construir una nueva "función" productora de c que consuma b. La flecha lo protege de consumir by crear un, cexcepto al participar en los diversos combinadores de flechas o al usar las operaciones primitivas de la instancia de flecha en particular.

Por ejemplo, las funciones de señal en Yampa son más o menos (Time -> a) -> (Time -> b), pero además deben obedecer una cierta restricción de causalidad : la salida en el tiempo testá determinada por los valores pasados ​​de la señal de entrada: no se puede mirar hacia el futuro. Entonces, lo que hacen es en lugar de programar con (Time -> a) -> (Time -> b), programar con SF a by construir sus funciones de señal a partir de primitivas. Sucede que, dado que se SF a bcomporta mucho como una función, esa estructura común es lo que se llama una "flecha".

Lambdageek
fuente
"La flecha lo protege de consumir by crear un, cexcepto al participar en los diversos combinadores de flechas o al usar las operaciones primitivas de la instancia de flecha en particular". Con disculpas por responder a esta antigua respuesta: esta oración me hizo pensar en tipos lineales, es decir, que los recursos no pueden ser clonados o desaparecidos. ¿Crees que podría haber alguna conexión?
glaebhoerl
14

Me gusta pensar que las Flechas, como Mónadas y Functores, permiten que el programador haga composiciones exóticas de funciones.

Sin Mónadas o Flechas (y Functores), la composición de funciones en un lenguaje funcional se limita a aplicar una función al resultado de otra función. Con las mónadas y los functores, puede definir dos funciones y luego escribir un código reutilizable separado que especifique cómo esas funciones, en el contexto de la mónada en particular, interactúan entre sí y con los datos que se pasan a ellas. Este código se coloca dentro del código de enlace de la Mónada. Entonces, una mónada es una vista única, solo un contenedor para código de enlace reutilizable. Las funciones se componen de manera diferente dentro del contexto de una mónada de otra mónada.

Un ejemplo simple es la mónada tal vez, donde hay un código en la función de enlace, de modo que si una función A está compuesta con una función B dentro de una mónada tal, y B produce una nada, entonces el código de enlace garantizará que la composición de la dos funciones generan un Nothing, sin molestarse en aplicar A al valor Nothing que sale de B. Si no hubiera mónada, el programador tendría que escribir el código en A para probar una entrada Nothing.

Las mónadas también significan que el programador no necesita escribir explícitamente los parámetros que cada función requiere en el código fuente; la función de enlace maneja el paso de parámetros. Entonces, usando mónadas, el código fuente puede comenzar a parecerse más a una cadena estática de nombres de funciones, en lugar de parecer que la función A "llama" a la función B con los parámetros C y D: el código comienza a sentirse más como un circuito electrónico que como un máquina móvil: más funcional que imperativa.

Las flechas también conectan funciones junto con una función de enlace, proporcionando funcionalidad reutilizable y ocultando parámetros. Pero las Flechas pueden conectarse y componerse, y opcionalmente pueden enrutar datos a otras Flechas en tiempo de ejecución. Ahora puede aplicar datos a dos rutas de flechas, que "hacen cosas diferentes" a los datos, y volver a ensamblar el resultado. O puede seleccionar a qué rama de Flechas pasar los datos, dependiendo de algún valor en los datos. El código resultante se parece aún más a un circuito electrónico, con interruptores, retrasos, integración, etc. El programa se ve muy estático y no debería poder ver mucha manipulación de datos. Hay cada vez menos parámetros en los que pensar, y menos necesidad de pensar en qué valores pueden o no tomar los parámetros.

Escribir un programa Arrowized implica principalmente seleccionar las flechas disponibles en el estante, como divisores, interruptores, retrasos e integradores, levantar funciones en esas flechas y conectar las flechas para formar flechas más grandes. En la programación reactiva funcional con flechas, las flechas forman un bucle, con la entrada del mundo combinada con la salida de la última iteración del programa, de modo que la salida reacciona a la entrada del mundo real.

Uno de los valores del mundo real es el tiempo. En Yampa, la flecha de la función de señal envía el parámetro de tiempo de forma invisible a través del programa de computadora: nunca se accede al valor de tiempo, pero si conecta una flecha integradora al programa, generará valores integrados a lo largo del tiempo que luego puede usar para pasar a otras flechas

Robert Onslow
fuente
pero esto suena como un funtor aplicativo (un envoltorio alrededor de una función, que proporciona funciones auxiliares, en algún contexto específico, para reutilizar funciones ya existentes para los tipos empaquetados). Definitivamente necesito leer más para entender, pero tal vez usted pueda ayudar, señalando lo que me falta
Belun
3

Solo una adición a las otras respuestas: Personalmente, me ayuda mucho entender qué es ese concepto (matemáticamente) y cómo se relaciona con otros conceptos que conozco.

En el caso de las flechas, encontré útil el siguiente documento: compara mónadas, functores aplicativos (modismos) y flechas: los modismos son ajenos, las flechas son meticulosas, las mónadas son promiscuas por Sam Lindley, Philip Wadler y Jeremy Yallop.

También creo que nadie mencionó este enlace que puede proporcionarle algunas ideas y literatura sobre el tema.

Petr Pudlák
fuente