2011-12-21 15 views
17

Estoy trabajando en un programa que usa reactive-banana, y me pregunto cómo estructurar mis tipos con los bloques básicos de FRP.Dónde aplicar Comportamiento (y otros tipos) en FRP

Por ejemplo, aquí hay un ejemplo simplificado de mi programa real: decir que mi sistema se compone principalmente de widgets - en mi programa, fragmentos de texto que varían con el tiempo.

pudiera tener

newtype Widget = Widget { widgetText :: Behavior String } 

pero también podría tener

newtype Widget = Widget { widgetText :: String } 

y utilizar Behavior Widget cuando quiero hablar sobre el comportamiento variable en el tiempo. Esto parece hacer las cosas "más simples", y significa que puedo usar las operaciones Behavior más directamente, en lugar de tener que desempaquetar y volver a empaquetar los widgets para hacerlo.

Por otro lado, el primero parece evitar la duplicación en el código que realmente define widgets, ya que casi todos los widgets varían con el tiempo, y me encuentro definiendo incluso los pocos que no lo hacen con Behavior, ya que me permite combinarlos con los demás de una manera más consistente.

Como otro ejemplo, con ambas representaciones, tiene sentido tener una instancia Monoid (y quiero tener una en mi programa), pero la implementación de esta última parece más natural (ya que es simplemente una elevación trivial del lista monoid para el nuevo tipo).

(Mi programa real utiliza Discrete en lugar de Behavior, pero no creo que eso es relevante.)

Del mismo modo, debería utilizar Behavior (Coord,Coord) o (Behavior Coord, Behavior Coord) para representar un punto 2D? En este caso, el primero parece ser la elección obvia; pero cuando se trata de un registro de cinco elementos que representa algo así como una entidad en un juego, la elección parece menos clara.

En esencia, todos estos problemas pueden reducir a un:

Cuando se utiliza FRP, en qué capa debo aplicar el tipo Behavior?

(La misma pregunta se aplica a Event también, aunque en menor grado.)

Respuesta

5

Estoy de acuerdo con dflemstr's advice a

  1. Aislar la "cosa que cambia" tanto como sea posible.
  2. Agrupe "cosas que cambian simultáneamente" en una Behavior/Event.

y le gustaría ofrecer razones adicionales para estas reglas generales.

La cuestión se reduce a lo siguiente: desea representar un par (tupla) de los valores que cambian en el tiempo y la cuestión es si utilizar

a. (Behavior x, Behavior y) - un par de comportamientos

b. Behavior (x,y) - un comportamiento de pares

Razones para preferir una sobre la otra son

  • a sobre b.

    En una implementación push-conducido, el cambio de un comportamiento desencadenará un nuevo cálculo de todos los comportamientos que dependen de ella.

    Ahora, considere un comportamiento cuyo valor depende sólo del primer componente x del par. En la variante a, un cambio en el segundo componente y no recalculará el comportamiento. Pero en la variante b, el comportamiento se volverá a calcular, incluso aunque su valor no dependa del segundo componente. En otras palabras, se trata de dependencias de grano fino versus de grano grueso.

    Este es un argumento para el asesoramiento 1. Por supuesto, esto no es de mucha importancia cuando ambos comportamientos tienden a cambiar de forma simultánea, lo que da consejos 2.

    Por supuesto, la biblioteca debe ofrecer una manera de ofrecer fina -grandes dependencias incluso para la variante b. A partir de la versión de reactivo-banana 0.4.3, esto no es posible, pero no se preocupe por eso, por ahora, mi implementación impulsada va a madurar en versiones futuras.

  • b sobre a.

    Al ver que reactive-banana versión 0.4.3 no ofrece dynamic event switching, existen ciertos programas que solo puede escribir si coloca todos los componentes en un solo comportamiento. El ejemplo canónico sería un programa que tenga variable número de contadores, es decir, una extensión del ejemplo TwoCounter.hs. Usted tiene que representarlo como una lista de tiempos de cambio de los valores

    counters :: Behavior [Int] 
    

    porque no hay manera de no perder de vista una colección dinámica de los comportamientos todavía. Dicho esto, la próxima versión de reactive-banana incluirá el cambio dinámico de eventos.

    Además, siempre se puede convertir de una variante a la variante b sin ningún problema

    uncurry (liftA2 (,)) :: (Behavior a, Behavior b) -> Behavior (a,b) 
    
+1

Bueno, en el caso de 'Widget', tener solo un campo no era una simplificación, es mi situación real, así que no hay tuplas involucradas :) Gracias por la ayuda, sin embargo, debería ser muy útil en el futuro! Pondré el 'Comportamiento' dentro del nuevo tipo por ahora. Desearía poder aceptar ambas respuestas :) – ehird

6

Las reglas que uso en el desarrollo de aplicaciones de FRP, son:

  1. aislar la "cosa que cambia", como tanto como sea posible.
  2. Agrupe "cosas que cambian simultáneamente" en una Behavior/Event.

El motivo de (1) es que resulta más fácil crear y componer operaciones abstractas si los tipos de datos que utiliza son lo más primitivos posible.

La razón de esto es que las instancias como Monoid se pueden reutilizar para los tipos sin formato, como usted describió.

Tenga en cuenta que puede usar Lenses para modificar fácilmente los "contenidos" de un tipo de datos como si fueran valores en bruto, de modo que un "envoltorio/desenvolver" adicional no sea un problema, principalmente. (Vea this recent tutorial para una introducción a esta implementación particular de Lens; hay others)

La razón de (2) es que simplemente elimina la sobrecarga innecesaria. Si dos cosas cambian simultáneamente, "tienen el mismo comportamiento", por lo que deben modelarse como tales.

Ergo/tl; dr: Debe utilizar newtype Widget = Widget { widgetText :: Behavior String } debido a (1), y se debe utilizar Behavior (Coord, Coord) debido a (2) (ya que ambas coordenadas generalmente cambiar de forma simultánea).

+0

No creo que las lentes ayudan aquí - para usar el ejemplo 'Monoid', es que algo como 'f = liftA2 mappend' se convierte en' fab = Widget $ mappend (widgetText a) (widgetText b) '. Es cierto que los combinadores de levantamiento pueden aliviar este dolor. Sin embargo, no estoy seguro de lo que está tratando de decir al hacer referencia a mi ejemplo 'Monoid' - se suponía que era un argumento para el formulario' String', no el formulario 'Behavior String'. – ehird

+0

Sin embargo, sus reglas suenan muy bien, y tendré que pensarlo un poco más. Muchas gracias por publicar esto! No aceptaré esta respuesta todavía porque me gustaría escuchar otros puntos de vista y perspectivas, y es una pregunta bastante sutil. – ehird

+0

En su ejemplo de 'Widget', solo contiene' widgetText' que hace que el levantamiento sin procesar sea trivial. Si hubiera tenido más valores en un 'Widget', el levantamiento en bruto se vuelve mucho más complicado que levantar un 'Comportamiento' a través de una lente y realizar operaciones de esa manera. – dflemstr

Cuestiones relacionadas