16

La mayoría del procesamiento de datos se puede prever como una tubería de componentes, la salida de una alimentación en la entrada de otra. Una canalización de procesamiento típica es:marcos para representar el procesamiento de datos como una tubería

reader | handler | writer 

Como una lámina para el inicio de esta discusión, vamos a considerar una implementación orientada a objetos de esta tubería, donde cada segmento es un objeto. El objeto handler contiene referencias tanto a los reader y writer objetos y tiene un método run que se parece a:

define handler.run: 
    while (reader.has_next) { 
    data = reader.next 
    output = ...some function of data... 
    writer.put(output) 
    } 

Esquemáticamente las dependencias son:

reader <- handler -> writer 

Ahora supongamos que quiero interponer un nuevo segmento de la tubería entre el lector y el controlador:

reader | tweaker | handler | writer 

De nuevo, en este OO implementación, tweaker sería una envoltura alrededor del objeto reader y los métodos tweaker podría ser algo como (en algún código de pseudo-imperativo):

define tweaker.has_next: 
    return reader.has_next 

define tweaker.next: 
    value = reader.next 
    result = ...some function of value... 
    return result 

estoy descubriendo que esto no es una abstracción muy componibles. Algunos temas son:

  1. tweaker sólo se puede utilizar en el lado izquierdo de handler, es decir, que no se puede utilizar la aplicación anterior de tweaker para formar este gasoducto:

    lector | manejador | tweaker | escritor

  2. me gustaría explotar la propiedad asociativa de tuberías, de modo que este gasoducto:

    lector | manejador | escritor

podría expresarse como:

reader | p 

donde p es la tubería handler | writer. En esta implementación OO tendría que crear una instancia parcialmente el objeto handler

  1. algo de una reexpresión de (1), los objetos tienen que saber si los datos de "atracción" de "empuje" o.

Estoy buscando un marco (no necesariamente OO) para crear canalizaciones de procesamiento de datos que resuelva estos problemas.

Lo he etiquetado con Haskell y functional programming porque creo que los conceptos de programación funcional pueden ser útiles aquí.

Como objetivo, sería agradable ser capaz de crear una tubería así:

     handler1 
       /  \ 
reader | partition    writer 
        \  /
        handler2 

Por alguna perspectiva, las del shell Unix resuelve muchos de estos problemas con las siguientes decisiones de implementación:

  1. componentes de tuberías se ejecutan de forma asíncrona en procesos separados

  2. canalizar objetos median pasar datos entre "empujadores" y "pulle rs "; es decir, bloquean a los escritores que escriben datos demasiado rápido y los lectores que intentan leer demasiado rápido.

  3. Se utilizan conectores especiales y <> para conectar componentes pasivos (es decir, archivos) a la tubería

Estoy especialmente interesado en los enfoques que no utilizan roscado o de paso de mensajes entre los agentes. Tal vez esa sea la mejor manera de hacerlo, pero me gustaría evitar el enhebrado si es posible.

Gracias!

+9

Eche un vistazo a http://www.haskell.org/arrows –

+1

Quizás le gustaría generar algunos hilos, uno para cada lector, tweaker, manejador y escritor, y comunicarse a través de ['Chan's ] (http://hackage.haskell.org/packages/archive/base/latest/doc/html/Control-Concurrent-Chan.html)? Aunque no estoy 100% seguro de entender lo que es la pregunta de nivel superior ... –

+0

Hasta ahora, el último diagrama se ve como 'reader >>> partition >>> handler1 *** handler2 >>> writer', pero probablemente haya algunos requisitos que lo hagan más complicado. –

Respuesta

20

Sí, arrows son casi seguramente su hombre.

Sospecho que eres bastante nuevo en Haskell, solo en función del tipo de cosas que dices en tu pregunta. Las flechas probablemente parezcan bastante abstractas, especialmente si lo que estás buscando es un "marco". Sé que me tomó un tiempo realmente asimilar lo que estaba pasando con las flechas.

Así que puede mirar esa página y decir "sí, eso se ve como lo que quiero", y luego encontrarse bastante perdido en cuanto a cómo comenzar a utilizar las flechas para resolver el problema. Así que aquí hay un poco de orientación para que sepas lo que estás mirando.

Las flechas no resolverán su problema. En su lugar, le dan un lenguaje que puede usar para expresar su problema. Puede encontrar que alguna flecha predefinida hará el trabajo, quizás alguna flecha kleisli, pero al final del día va a querer implementar una flecha (las predefinidas solo le dan formas fáciles de implementarlas) que expresa lo que quieres decir con un "procesador de datos". Como ejemplo casi trivial, digamos que desea implementar sus procesadores de datos mediante funciones simples. Se podría escribir:

newtype Proc a b = Proc { unProc :: a -> b } 

-- I believe Arrow has recently become a subclass of Category, so assuming that. 

instance Category Proc where 
    id = Proc (\x -> x) 
    Proc f . Proc g = Proc (\x -> f (g x)) 

instance Arrow Proc where 
    arr f = Proc f 
    first (Proc f) = Proc (\(x,y) -> (f x, y)) 

Esto le da la maquinaria a utilizar los diversos combinadores de flecha (***), (&&&), (>>>), etc., así como la flecha notación que es bastante bueno si usted está haciendo las cosas complejas. Así, como señala Daniel Fischer en el comentario, la tubería que ha descrito en su pregunta podría estar compuesto como:

reader >>> partition >>> (handler1 *** handler2) >>> writer 

Pero lo bueno es que depende de usted lo que quiere decir por un procesador. Es posible poner en práctica lo que usted ha mencionado sobre cada procesador se bifurcan un hilo de una manera similar, utilizando un tipo de procesador diferente:

newtype Proc' a b = Proc (Source a -> Sink b -> IO()) 

Y a continuación, la aplicación de los combinadores de manera apropiada.

Así que eso es lo que está viendo: un vocabulario para hablar sobre procesos de composición, que tiene un poco de código para reutilizar, pero principalmente ayudará a guiar su pensamiento mientras implementa estos combinators para la definición de procesador que es útil en tu dominio.

Uno de mis primeros proyectos no triviales de Haskell fue implementar un arrow for quantum entanglement; ese proyecto fue el que me hizo comenzar realmente a entender la forma de pensar de Haskell, un punto de inflexión importante en mi carrera de programación. ¿Tal vez este proyecto tuyo hará lo mismo por ti? :-)

7

Gracias a evaluación diferida, podemos expresar las tuberías en términos de composiciones de funciones normales en Haskell. Aquí un ejemplo que calcula la longitud máxima de una línea en un archivo:

main = interact (show . maximum . map length . lines) 

Todo aquí es una función ordinaria, como por ejemplo

lines :: String -> [String] 

pero gracias a la evaluación perezosa, estas funciones único proceso ingrese incrementalmente y solo tanto como sea necesario, tal como lo haría una tubería UNIX.

+0

Gracias por este ejemplo. – ErikR

4

El enumerator package para Haskell es un buen marco para esto. Define tres tipos de objetos:

  1. Enumeradores que producen datos en trozos.
  2. Iteraciones que consumen trozos de datos y devuelven un valor después de consumir suficiente.
  3. Enumerados que se encuentran en el medio de la tubería. Consumen trozos y producen trozos, posiblemente con efectos secundarios.

Estos tres tipos de objetos se componen en un canal de procesamiento de flujo, e incluso puede tener varios enumeradores e iterados en una canalización (cuando uno termina, el siguiente toma su lugar). Puede ser complicado escribir uno de estos objetos desde cero, pero hay muchos combinadores que se pueden usar para convertir funciones normales en procesadores de flujo de datos. Por ejemplo, esta tubería lee todos los caracteres de la entrada estándar, los convierte a mayúsculas con la función toUpper, a continuación, los escribe en la salida estándar:

ET.enumHandle stdin $$ ET.map toUpper =$ ET.iterHandle stdout 

donde el módulo de Data.Enumerator.Text ha importado como ET.

+2

Hay varios paquetes de estilo de enumeración en Hackage; el OP podría estar interesado en iter-io (http://hackage.haskell.org/package/iterIO), que se basa explícitamente en las tuberías del shell de Unix. –

2

El marco Yesod hace uso de una biblioteca de tubos Haskell en forma del paquete conduit.

Cuestiones relacionadas