« Elm: Introducción « || Inicio || » Elm: Sintaxis Básica… »

Elm: Señales

Última modificación: 25 de Noviembre de 2016, y ha tenido 298 vistas

Etiquetas utilizadas: || ||

En las entradas anteriores (Introducción y Elementos Visuales) trabajamos con textos y algunos gráficos, pero solo mostrando información estática. En esta entrada introduciremos las herramientas necesarias para poder trabajar con eventos dentro de las páginas que diseñemos con Elm. Para ello, haremos uso de algunos conceptos de lo que se conoce como Programación Funcional Reactiva, como es el concepto de señal. Es necesario que tengas clara la teoría que hay detrás de esta metodología, así que te recomiendo ver esta otra entrada en este mismo sitio. Iremos viendo los eventos más habituales con los que tenemos que lidiar a la hora de hacer aplicaciones web reactivas, y en los ejercicios asociados a esta entrada podrás practicar combinaciones interesantes de ellos para crear efectos más elaborados.

La Programación Funcional Reactiva Clásica es un modelo muy potente, posiblemente la más potente de las variantes que existen de entre las reactivas, pero tiene un grave problema de gestión de memoria. Se podrían añadir que ocurren en cualquier momento, incluso pasados y futuros, lo que significa que se necesita almacenar todos los eventos pasados para recalcular el estado actual. Además, se puede crear una nueva señal en cualquier momento, y esta señal podría depender de eventos pasados y de eventos futuros, lo que  aveces puede tener una interpretación poco clara. En consecuencia, es muy flexible pero podría usar una cantidad excesiva de memoria, o necesitar demasiada computación, para proporcionar esas posibilidades.

El modelo que sigue Elm es ligeramente distinto, no hay señales de primera clase, no puedes crear señales, no puedes destruirlas y no las puedes pasar como valores. Dispones de un conjunto de operadores sobre señales que permiten combinarlas, transformarlas y filtrarlas (tal y como vimos en la entrada sobre PFR). Estos operadores se construyen de forma que nunca necesitan el conjunto entero de eventos pasados ni de eventos futuros.

Elm combina señales continuas y eventos en un único concepto llamado Signal, que es como un evento en el sentido en que puede tener solo una cantidad numerable de ocurrencias, y como las señales continuas en el sentido en que siempre tiene definido un valor. Esta diferencia hace que Elm no se ajuste completamente al concepto de PFR, aunque lo hace funcionar de manera muy eficiente en la mayoría de los casos. Además, para este tipo de señales tampoco hay definida una semántica denotacional formal por el momento.

Signal: Señales formales en Elm

Como hemos comentado, una señal es un valor que cambia en el tiempo... por tanto, no tiene sentido como un tipo en si mismo, sino como un modificador de un tipo dado. Por ejemplo, Signal Int será una señal de valores enteros.

De igual forma, podemos encontrar señales de cualquier tipo, por ejemplo, Signal List Int (señal de listas de enteros), Signal Float (señal de valores decimales), Signal (Int,Int) (señal de pares de enteros), Signal Element (señal de elementos renderizables), Signal Html, etc...

Formalmente, Signal realmente es lo que se conoce como un Functor Aplicativo, y también está relacionado con el concepto de Mónada. No veremos aquí (al menos no ahora, quizás en una entrada posterior) qué significa esto, pero es interesante que al menos el lector sepa que la forma en que se entienden las señales tiene un maerco teórico bien establecido.

Es importante señalar un conjunto de ideas básicas que proporcionan las señales:

  1. No tenemos que actualizarlas explícitamente, siempre tienen el valor más reciente.
  2. Los cambios de una señal se propagan automáticamente a las señales dependientes.
  3. Representa un valor mutable dentro de la puerza del mundo funcional. Se convierte en una forma muy elegante de introducir la impureza dentro de la pureza e inmutabilidad del paradigma funcional, dando como resultado un código muy limpio, evitando soluciones sucias e ineficientes comunes en otros paradigmas.

En consecuencia, en Elm las señales son las conexiones de nuestros programas con el mundo real. Los diferentes tipos de señales que veremos en esta entrada son:

Señales de Ratón

Vamos a comenzar añadiendo algunas reacciones a los eventos del ratón. Elm maneja los eventos del ratón por medio de lo que se llaman señales. Antes de describir qué son, veámoslas en acción. Para ello, abre de nuevo el editor de prueba que proporciona Elm e introduce el siguiente programa:

import Graphics.Element exposing (..)
import Mouse
    
main : Signal Element
main =
    Signal.map show Mouse.x

Al ejecutar el programa anterior, se muestra la coordenada x del ratón de forma dinámica (es decir, que reacciona en tiempo real a los eventos de éste, y si movemos el ratón sobre el área de ejecución, la coordenada representada se actualiza automáticamente). 

La expresión Mouse.x representa una señal de la coordenada x del ratón. Como ya explicamos anteriormente, una señal es un flujo de valores que cambian a lo largo del tiempo. A medida que el cursor del ratón cambia su posición, el valor de la señal que representa su coordenada también cambia. Una señal siempre está definida, siempre tiene un valor. El valor inicial de esta señal en particular es 0 (puedes verificarlo al arrancar el programa antes de mover el ratón), en cuanto mueves el ratón la señal cambia para almacenar el valor real, y si paras de moverlo, la señal permanece con el último valor obtenido.

Para poder trabajar con las funciones que manipulan el ratón hemos de importar previamente la librería Mouse, donde podemos encontrar formas de manipular las coordenadas o las pulsaciones de los botones (puedes encontrar información detallada de esta librería aquí).

El tipo de Mouse.x es Signal Int, lo que indica que es una señal de valores de tipo Int. Estos valores no los podemos mostrar directamente, ya que las señales no son objetos que puedan ser renderizados por el navegador web, por lo que no podemos hacer algo como main = Mouse.x, ya que main no sabe qué hacer con este tipo de dato.

En entradas anteriores habíamos comentado que main era de tipo representable, generalmente Element, pero la realidad es que es un poco más general y puede devolver también algo de tipo Signal Element (es decir, un elemento estático o uno dinámico, que varía en el tiempo), e incluso Signal Html...

Ya vimos que podíamos usar show para convertir datos en elementos representables, pero ahora necesitamos convertir un Signal Int en un Signal Element. Para ello se hace uso de la función Signal.map, una función que permite elevar valores normales a señales:

Signal.map : (a -> b) -> Signal a -> Signal b

Es decir, toma una función y una señal y aplica la función a los valores que la señal arrastra... o lo que es lo mismo, aplica la función "dentro" de la señal de tipo a, convirtiéndola en una señal de valores de tipo b. En cierta forma, es el equivalente a List.map que existe para listas:

Un ejemplo similar, para mostrar si el botón del ratón está siendo pulsado o no, es el siguiente:

import Graphics.Element exposing (..)
import Mouse
    
main : Signal Element
main =
    Signal.map show Mouse.isDown

Obviamente, poco podríamos hacer si únicamente podemos capturar una señal. En el siguiente ejemplo mostramos cómo podemos trabajar con más de una señal simultáneamente. Para ello capturaremos dos señales y las combinaremos para mostrar el par de coordenadas del puntero del ratón:

import Graphics.Element exposing (..)
import Mouse
import Signal exposing (..)
    
main : Signal Element
main = 
    Signal.map2 combina Mouse.x Mouse.y
    
combina : Int -> Int -> Element
combina x y =  show (x,y)

Esta función mezcla los valores de las señales Mouse.x y Mouse.isDown haciendo uso de la función combina, que hemos defnido para que actúe adecuadamente. En el ejemplo anterior hemos tratado manualmente ambas señales, pero no mostramos cómo hacer uso de map para ese mismo resultado. El problema es que la función map no es suficiente cuando queremos combinar 2 señales para obtener una sola señal. Para esa labor tenemos la función map2, que tiene la siguiente signatura:

Signal.map2 : (a -> b -> c) -> Signal a -> Signal b -> Signal c

Haciendo un uso adecuado de map y map2 podriamos haber reescrito el programa anterior como sigue:

import Graphics.Element exposing (..)
import Mouse
import Signal

main : Signal Element
main =
Signal.map show (Signal.map2 (,) Mouse.x Mouse.y)

Un ejemplo un poco más elaborado podría ser el siguiente:

import Color exposing (..) 
import Graphics.Collage exposing (..) 
import Graphics.Element exposing (..) 
import Mouse 
import Window 
import Signal
    
main : Signal Element 
main = Signal.map2 escena Mouse.position Window.dimensions 
    
escena : (Int,Int) -> (Int,Int) -> Element 
escena (x,y) (w,h) = 
    let (dx,dy) = 
      (toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y) 
    in 
      collage w h 
      [ ngon 3 100 
      |> filled blue 
      |> rotate (atan2 dy dx) 
      ,ngon 6 30 
      |> filled orange 
      |> move (dx, dy) 
      ] 

Elm también proporciona funciones similares para combinar mayor número de señales: map3, map4,... 

Veamos a continuación un ejemplo que hace uso de varias señales simultáneamente:

import Mouse
import Signal exposing (..)
import Graphics.Element exposing (..)
    
muestrasenales a b c d e f=
    flow down (List.map show 
    [ ("Mouse.position: " ++ toString a)
    , ("Mouse.x: " ++ toString b)
    , ("Mouse.y: " ++ toString c)
    , ("Mouse.isDown: " ++ toString d)
    , ("Mouse.clicks: " ++ toString e)
    , ("Mouse.position cuando se ha pulsado: " ++ toString f)
    ])
    
main = Signal.map6 muestrasenales Mouse.position
Mouse.x Mouse.y Mouse.isDown Mouse.clicks sampleOn Mouse.isDown Mouse.position

La función muestrasenales representa una lista de varios valores con descripciones. Cada item de esta lista representa una señal distinta, que después se combinan.

La señal Mouse.clicks es una señal de valores de tipo (). En este caso, () es simultáneamente el tipo y el único valor de ese tipo (es lo que se llama una unidad):

> ()
    () : ()

Su función es la de generar un nuevo evento cada vez que se pulsa el botón del ratón (más concretamente, cuando se suelta el botón). Es por ello que, como puede comprobarse al ejecutar el programa, el resultado de la ejecución de esta función no devuelve, en principio, más que ese valor, de poca utilidad. Ya veremos a continuación cómo sacarle provecho.

La señal Mouse.isDown es una señal de valores booleanos que indica si el botón está siendo pulsado o no.

El último ejemplo de cómo manipular señales del ejemplo anterior usa la función sampleOn, que recibe dos señales y devuelve la segunda señal cada vez que la primera señal genera un evento.

sampleOn : Signal a -> Signal b -> Signal b

Por tanto, en nuestro caso representa un flujo de señales con las posiciones en el momento en que el ratón se ha pulsado.

Señales de Ventanas

El módulo Window de la librería estándar define señales que proporcionan información acerca de las dimensiones del contenedor en el que vive el  programa de Elm. Este contenedor puede ser la ventana completa del navegador, pero no tiene porqué (por ejemplo, podría ser un elemento div embebido en una página HTML).

En el ejemplo que mostramos a continuación se tiene un programa no embebido, y por tanto las señales representarán las dimensiones de la ventana del navegador. Pero si se ejecuta en el editor online del sitio oficial de Elm, obtendremos el tamaño de la ventana en el que se ejecuta, que podemos modificar por medio del deslizador y ver cómo cambia en tiempo real:

import Window
import Graphics.Element exposing (..)
import Signal exposing (..)
    
muestrasenales a b c =
    flow down ( List.map show
    [ "Window.dimensions: " ++ toString a
    , "Window.width: " ++ toString b
    , "Window.height: " ++ toString c
    ])
    
main = Signal.map3 muestrasenales Window.dimensions
                                  Window.width
                                  Window.height

En este programa se presentan 3 señales:

  • Window.dimensions representa pares (tuplas) con la achura y altura del contenedor,
  • Window.width representa la anchura,
  • Window.height representa la altura.

Señales del Teclado

El módulo Keyboard (del que puedes encontrar más detalles aquí) define funciones que son capaces de manipular la entrada por teclado. Elm define el tipo Keycode como un alias para Int y que sirve para expresar los valores de entrada por teclado. Podemos usar las funciones toCode y fromCode de la librería Char para convertir valores entre los tipos KeyCode y Char (caracteres). Ha de tenerse en cuenta que la función toCode devuelve el código ASCII de la letra mayúscula asociada (aunque el dato de entrada sea una minúscula). El código que se muestra a continuación ha sido extraído de una sesión con elm-repl:

> import Char (toCode, fromCode)
> import Keyboard (..)
> toCode 'a'
65 : Int
> toCode 'A'
65 : Int
> toCode '2'
50 : Int
> fromCode 65
"A" : Char
> fromCode 97
"a" : Char

La librería Keyboard tiene funciones como arrows o wasd, orientadas al control de direcciones con el teclado, que provocan señales de enteros para indicar qué tecla de ese grupo se está ha presionado (x=1, derecha; x=-1, izquierda;y=1, arriba; y=-1, abajo).

import Graphics.Element exposing (..)
import Keyboard
    
main : Signal Element
main =
    Signal.map show Keyboard.arrows

Si en el código anterior sustuituimos Keyboard.arrows por Keyboard.wasd, estaremos considerando la pulsación de las teclas wasd para el control de las dirección.

La función presses devuelve una señal con el código de la última tecla pulsada, y la función keysDown crea una señal con el conjunto de los códigos de las teclas que están siendo pulsadas (es un conjunto porque se pueden mantener pulsadas varias teclas a la vez). 

import Graphics.Element exposing (..)
import Keyboard
import Set
import Char
import Signal exposing (..)
    
muestra : Set.Set Int -> Int -> Element
muestra codigosTeclas codigoTecla =
    flow down ( List.map show
    [ "Teclas pulsadas: " ++ toString (Set.toList codigosTeclas)
    , "Última tecla pulsada: " ++ toString (Char.fromCode codigoTecla)
    ])
    
main : Signal Element
main = Signal.map2 muestra Keyboard.keysDown Keyboard.presses

La función isDown toma un código de tecla como argumento y devuelve una señal booleana indicando si esa tecla está actualmente pulsada o no. Y también hay algunas funciones auxiliares que permiten saber si la tecla pulsada se corresponde con las teclas especiales (shift, ctrl, enter, etc.).

Señales Temporales

La librería Time (puedes encontrarla aquí) define funciones y señales que permiten trabajar con el tiempo. Elm define el tipo Time como un alias de Float, que denota el número de milisegundos.

Las primeras herramientas que debemos conocer proporcionan funciones para manipular las relaciones entre las diversas unidades de tiempo y los milisegundos, que es la unidad por defecto que se usa:

> import Time (..)
> millisecond
1 : Float
> 2*second
2000 : Float
> 4*minute
240000 : Float
> hour
3600000 : Float

Y también funciones para convertir valores en la dirección contraria:

> inSeconds 2000
2 : Float
> inMinutes 90000
1.5 : Float
> inHours 7200000
2 : Float
> inMilliseconds 4
4 : Float

Junto a estas funciones de conversión, el módulo define otras funciones que permiten crear señales relacionadas con el tiempo. Veamos su uso en un programa:

import Time exposing (..)
import Mouse
import Graphics.Element exposing (..)
import Signal exposing (..)
    
muestrasenales a b c d e f =
    flow down (List.map show
    [ "every (5*second): " ++ toString a
    , "since (2*second) Mouse.clicks: " ++ toString b
    , "timestamp Mouse.isDown: " ++ toString c
    , "delay second Mouse.position: " ++ toString d
    , "fps 200: " ++ toString e
    , "fpsWhen 200 Mouse.isDown: " ++ toString f
    ])
    
main = Signal.map6 muestrasenales every (5*second)
                                  since (2*second) Mouse.clicks
                                  timestamp Mouse.isDown
                                  delay second Mouse.position
                                  fps 200
                                  fpsWhen 200 Mouse.isDown

A continuación damos una breve explicación de qué hace cada una de las señales usadas en el código anterior:

  • La primera señal, every n (every: Time -> Signal Time), devuelve el tiempo actual cada n milisegundos (realmente, devuelve un Signal Time). Se debe tener en cuenta que cada uso distinto de este comando produce señales independientes.
  • La segunda señal, since (since : Time -> Signal a -> Signal Bool), recibe como dato de entrada un tiempo y una señal, y devuelve una señal booleana que es verdad durante el tiempo indicado a que se produzca un evento de la señal a. Por tanto, en este caso, since devuelve una señal booleana que es verdad durante los 2 segundos posteriores a cada pulsación del ratón (aquí vemos un ejemplo de uso de Mouse.clicks, al que hicimos mención en el apartado de señales del ratón).
  • La tercera señal, timestamp (timestamp: Signal a -> (Time, Signal a)), añade una marca temporal a cualquier otra señal en el instante en que se produce su evento asociado. Por tanto, en el ejemplo, devuelve un par de valores: el primero de ellos es el tiempo en el que se produjo la señal Mouse.isDown, y el segundo es el valor de la señal Mouse.isDown cuando se produjo el evento.
  • La cuarta señal, delay (delay: Time -> Signal a -> Signal a), toma un valor temporal, t, y una señal y crea otra señal con la misma salida pero con el retraso indicado por t.. En el ejemplo, devuelve Mouse.position pero con un retraso de 1 segundo.
  • La quinta señal, que hace uso de fps (fps: number -> Signal Time), devuelve eventos tan rápido como sea posible, pero limitados por el número que recibe como entrada (en este caso, 200 eventos por segundo). En ordenadores rápidos se podrá llegar a esta cota, en los más lentos es posible que se quede por debajo. El valor emitido en cada evento es el lapso de tiempo transcurrido entre el último evento emitido y el actual (en milisegundos, claro). Al igual que ocurría con every, usos distintos de este comando produce señales independientes.
  • La última señal, fpsWhen (fpsWhen: number -> Signal Bool -> Signal Time), es igual que la anterior, pero solo emite la señal temporal si la señal booleana es true. En el caso del ejemplo, se intentarán alcanzar los 200 fps mientras se mantenga pulsado el botón del ratón.

El siguiente ejemplo muestra cómo hacer uso de las señales temporales para crear una animación automática de elementos gráficos:

import Color exposing (..)
import Graphics.Collage exposing (..)
import Graphics.Element exposing (..)
import Time exposing (..)
    
main =
    Signal.map reloj (every second)
    
reloj t =
    collage 400 400
    [ filled lightGrey (ngon 12 110)
    , outlined (solid grey) (ngon 12 110)
    , aguja orange 100 t
    , aguja charcoal 100 (t/60)
    , aguja charcoal 60 (t/720)
    ]
    
aguja col long tiempo =
    let
      angulo = degrees (90 - 6 * inSeconds tiempo)
    in
      segment (0,0) (fromPolar (long,angulo)) |> traced (solid col)

Pantallas táctiles

Si dispones de una pantalla táctil puedes hacer uso de la librería Touch, que aún siendo todavía muy sencilla te permite trabajar con los toques y arrastres que se hagan con la pantalla táctil, como puedes observar en este ejemplo:

import Graphics.Element exposing (..)
import Touch exposing (..)
import Signal
    
muestrasenales a b c=
    flow down ( List.map show
    [ "Número de Toques: " ++ toString a
    , "ültimo Toque: " ++ toString b
    , "Último Arrastre: " ++ toString c
    ])
    
main = Signal.map3 muestrasenales cuentaTaps 
Touch.taps
Touch.touches cuentaTaps : Signal Int cuentaTaps = Signal.foldp (\tap total -> total + 1) 0 Touch.taps

Combinando Señales

Ya vimos en la entrada que hablaba de la Programación Funcional Reactiva en general que la forma de sacarle potencia al trabajo con señales pasaba por el uso de combinadores que permitían operar sobre diversas señales simultáneamente para obtener árboles de procesos de señales más complejos. En los ejemplos anteriores hemos usado ya un operador que funciona de forma muy similar al map ya conocido por su forma de trabajar sobre listas, y que aquí se denomina Signal.map (o en sus versiones con mayor aridad, Signal.map2, Signal.map3, etc.).

Uno de los operadores más interesantes, porque permite tratar de manera recursiva las señales, es Signal.foldp (de fold past) que se comporta de manera muy similar a como lo haría foldl sobre una lista, pero considerando que el plegado lo estamos haciendo en el tiempo en el que se desarrolla la señal, y no en las posiciones de la lista.

A continuación mostramos algunos ejemplos de su uso que, además, hacen uso de las señales que hemos visto en las secciones anteriores.

Un ejemlo muy simple es el siguiente, en el que se usa foldp para ir contando las veces que se recibe una señal de pulsación del ratón:

import Graphics.Element exposing (..)
import Mouse
import Signal
    
main : Signal Element
main = Signal.map show cuentaClics
    
cuentaClics : Signal Int
cuentaClics = Signal.foldp (\clic total -> total + 1) 0 Mouse.clicks

En el siguiente ejemplo hacemos uso de Signal.foldp para obtener una lista con las diversas localizaciones en las que se ha pulsado con el ratón. Es interesante observar también el uso de Signal.sampleOn para obtener la señal Mouse.position únicamente cuando el usuario ha pulsado el botón del ratón, y no continuamente. Para ver la diferencia, cambia la función localizaciones por Signal.foldp (::) [] Mouse.position:

import Graphics.Element exposing (..)
import Mouse
    
main : Signal Element
main = Signal.map show localizaciones
    
localizaciones : Signal (List (Int,Int))
localizaciones =
    Signal.foldp (::) [] (Signal.sampleOn Mouse.clicks Mouse.position)

Podemos aprovechar el ejemplo anterior para construir un ejemplo más completo en el que aprovechamos las coordenadas que va pulsando el usuario para ir situando círculos:

import Color exposing (..)
import Graphics.Collage exposing (..)
import Graphics.Element exposing (..)
import Mouse
import Window
import Signal
    
main : Signal Element
main = Signal.map2 escena Window.dimensions 
localizaciones localizaciones : Signal (List (Int,Int)) localizaciones = Signal.foldp (::) [] (Signal.sampleOn Mouse.clicks Mouse.position) escena : (Int,Int) -> List (Int,Int) -> Element escena (w,h) locs = layers [ flow down [ show "Pulsa donde quieras un círculo:" , show locs ] , collage w h (List.map (circulo (w,h)) locs) ] circulo : (Int,Int) -> (Int,Int) -> Form circulo (w,h) (x,y) = circle 20 |> filled (rgba 0 0 255 0.2) |> move (toFloat x - toFloat w / 2, toFloat h / 2 - toFloat y)

Un ejemplo de animación haciendo uso de las señales temporales y de la función foldp podría ser el siguiente:

import Graphics.Element as GE
import Graphics.Collage as GC
import Time
import Signal
    
textForm texto posicion = GE.show texto |> GC.toForm |> GC.move posicion
    
render t = GC.collage 200 200 
    [ textForm "Horizontal" ((sin t)*50,0) |> GC.rotate (t*2)
    , textForm "Vertical" (50,(sin t)*25) |> GC.rotate (0-t/4) ]
    
main = Signal.map render (Signal.foldp (\t acc -> acc + (t/500)) 0 (Time.fps 24))

Otro combinador que puede ser interesante es Signal.merge, que combina dos señales en una (para que la combinación sea efectiva suele ser necesario definir un tipo de dato que sea capaz de contener ambos tipos de señales simultáneamente, como muestra el ejemplo siguiente). Merge puede resultar muy útil para poder combinar varios tipos de señales que deben ser procesados de forma conjunta por un foldp.

import Graphics.Element exposing (..)
import Mouse
import Time
import Signal
    
type Combinacion = MovimientoRaton (Int,Int) | LapsoTiempo Float
    
main : Signal Element
main = Signal.map show combinada
    
combinada : Signal Combinacion
combinada =
    Signal.merge
      (Signal.map MovimientoRaton Mouse.position)
      (Signal.map LapsoTiempo (Time.fps 40))

Si alguna de las señales que se están mezclando se actualiza, entonces se produce una señal de salida combinada, y si ambas señales llegasen exactamente a la vez, entonces se considera la de más a la izquierda.

Elm además proporciona una variante, llamada mergeMany, que permite combinar una lista de señales de forma similar a como lo hace merge:

mergeMany : List (Signal a) -> Signal a

También existe un función Signal.filter (filter : (a -> Bool) -> a -> Signal a -> Signal a) que permite filtrar los valores de una señal dependiendo de un predicado. Así, si se verifica el predicado, la señal pasa sin problemas, y si no se verifica es como si no se hubiera recibido.

Un primer ejemplo puede ser el siguiente:

import Graphics.Element exposing (..)
import Mouse
import Signal
    
main : Signal Element
main = Signal.map show cuentaClics
    
cuentaClics : Signal Int
cuentaClics = Signal.filter par 0 (Signal.foldp (\clic total -> total + 1) 0 Mouse.clicks)
    
par : Int -> Bool
par x = x `rem` 2 == 0

Donde se procesan solo las señales que indican una cantidad par de pulsaciones del ratón. Otro ejemplo, también usando uno de los ejemplos que vimos anteriormente, puede ser:

import Graphics.Element exposing (..)
import Mouse
    
main : Signal Element
main = Signal.map show localizaciones
    
localizaciones : Signal (List (Int,Int))
localizaciones =
    Signal.foldp (::) [] (Signal.filter enRango (0,0) (Signal.sampleOn Mouse.clicks 
Mouse.position)) enRango : (Int,Int) -> Bool enRango (x,y) = x < 300 && y < 300

En este caso, si la posición del ratón al pulsar el botón está fuera del rango \((0,300)\times (0,300)\), la señal es ignorada, si está dentro de ese rango pasa de forma normal. En caso de que el resultado de filter haya de ser procesado completamente y no haya pasado ninguna señal el filtro, se usa el valor por defecto que se le pasa como segundo parámetro (en el ejemplo, el par \((0,0)\)).

También podemos eliminar las nuevas señales que se van produciendo si son iguales a las últimas recibidas (de forma que podemos quedarnos solamente con las señales que producen cambios en el sistema) por medio de Signal.dropRepeats. Por ejemplo, trabajando sobre un ejemplo anterior, podemos hacer:

import Graphics.Element exposing (..)
import Mouse

main : Signal Element
main = Signal.map show localizaciones
    
localizaciones : Signal (List (Int,Int))
localizaciones =
    Signal.foldp (::) [] (Signal.dropRepeats Mouse.position)

Un uso específico de señales para la construcción de sistemas interactivos viene dado por lo que en Elm se conocen como Mailboxes, pero todo eso lo veremos en una entrada posterior.

En la siguiente entrada estudiaremos un proyecto completo que hace uso de señales sobre elementos visuales y propondremos otros casos de estudio interesantes. Los ejemplos que veremos nos servirán también para introducir la arquitectura Modelo-Vista-Controlador que usaremos en los proyectos a desarrollar.

Algunas notas más sobre señales en Elm

Un caso habitual donde podemos encontrarnos con algunas limitaciones con el tratamiento de las señales por parte de Elm podría ser el siguiente. Supongamos que tenemos una lista de widgets, cada uno con su propio estado y comportamiento. Lo más natural sería que cada uno de los widgets tuviera su propia señal, pero entonces deberíamos poder manejar una rede de procesos de señales dinámica. En Elm tenemos que mantener una estructura con todas las señales (o una lista de señales) y cada vez que alguna se actualiza, realmente se actualiza toda la estructura completa (o la lista). Desde un punto de vista de rendimiento, esta solución es menos eficiente que poder manipular cada señal por separado.Un caso similar se daría si nuestra aplicación consta de un conjunto de partes completamente independientes... la aplicación completa tiene que mantener una estructura (de señal) que agrupe todas las señales independientes, aunque la mayoría de ellas no puedan actualizarse cuando se está ejecutando una de las partes que no le corresponden. En cualquier caso, esta solución aquí no es tan mala porque es fácil suprimir las actualizaciones de un conjunto de señales en función dle estado del programa.

Una situación que con el modelo de Elm no está resuelta todavía es la que necesita usar señales interdependientes. Por ejemplo, el caso de dos señales de entrada en las que cualquier de ellas permite reproducir la otra, pero dependiendo del instante, podemos estar recibiendo una u otra. En Elm, obligatoriamente, hemos de definir una de las dos en función de la otra en caso de que queramos reflejar esta dependencia, o bien dejarlas como lecturas independientes.

« Elm: Introducción « || Inicio || » Elm: Sintaxis Básica… »