Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this Reflex code result in Dynamics firing indefinitely with the same value?

The intent of this small program is to show three buttons, with the third button's label initially being "0" and afterwards being the index of the last-clicked button. For now the number of buttons and the labels of the other buttons are constant.

When I compile this self-contained file with ghcjs and load Main.jsexe/index.html in the browser, I can see the two traceDyns firing in a loop, both always having the value 0. As far as I understand, nothing should happen until a button is clicked, because the _el_clicked feeds the rest of the system.

Also, note that I'm using mapDyn (fst . head . Map.toList) in order to extract the index of the selected button - I'm not sure this is correct, but either way I don't know what causes the infinite looping.

{-# LANGUAGE RecursiveDo #-}

module Main where

import Reflex
import Reflex.Dom

import qualified Data.Map as Map

dynButton
  :: MonadWidget t m
  => Dynamic t String
  -> m (Event t ())
dynButton s = do
  (e, _) <- el' "button" $ dynText s
  return $ _el_clicked e

-- widget that takes dynamic list of strings
-- and displays a button for each, returning
-- an event of chosen button's index
listChoiceWidget
  :: MonadWidget t m
  => Dynamic t [String]
  -> m (Event t Int)
listChoiceWidget choices = el "div" $ do
  asMap <- mapDyn (Map.fromList . zip [(0::Int)..]) choices
  evs <- listWithKey asMap (\_ s -> dynButton s)
  k <- mapDyn (fst . head . Map.toList) evs
  return $ updated (traceDyn "k" k)

options :: MonadWidget t m => Dynamic t Int -> m (Dynamic t [String])
options foo = do
  mapDyn (\x -> ["a", "b", show x]) foo

main :: IO ()
main = mainWidget $ el "div" $ do
  rec n <- listChoiceWidget o
      o <- options foo
      foo <- holdDyn 0 n
  display (traceDyn "foo" foo)
like image 951
ajp Avatar asked Jun 17 '15 08:06

ajp


1 Answers

It looks like your code for listChoiceWidget is throwing away the click events constructed by dynButton.

listWithKey returns m (Dynamic t (Map k a)). In your case, the keys are of type Int and the values are Event t () (produced by dynButton).

On this line:

k <- mapDyn (fst . head . Map.toList) evs

You are turning the Dynamic t (Map Int (Event t ())) into a Dynamic t Int but, crucially, you're not doing so when a click event fires. This line maps over evs and produces a Dynamic that will always contain the first key in the Map of Ints to Events, regardless of whether an event has fired or not. It will always be a Dynamic containing the Int 0.

The reason you're seeing a loop is because:

  1. main feeds foo with its initial value of 0 into options
  2. new options are constructed
  3. listChoiceWidget receives the new options and the list is updated
  4. the first key of the resultant Map of Ints to Events is updated
  5. foo receives the key updated event from listChoiceWidget
  6. back to step 2 ad infinitum

Instead of retrieving the first key out of the Map, you need some way of determining the last button click event. Your Map already contains click events for each button displayed. Right now those events have the type Event t (), but what you really need is Event t Int, so that when an event fires you can tell which button it came from.

evs' <- mapDyn (Map.mapWithKey (\k e -> fmap (const k) e)) evs

evs' has the type Dynamic t (Map Int (Event t Int)). Next we need some way of combining our events so that we have one event that fires with the most recently clicked button's key.

dynEv <- mapDyn (leftmost . Map.elems . Map.mapWithKey (\k e -> fmap (const k) e)) evs

dynEv now has the type Dynamic t (Event t Int). The keys of the Map have already been baked into the events, so we don't need them anymore. Map.elems turns our Map of events into a list of events, and leftmost allows you to combine a list of events into one event.

From the docs for leftmost: "Create a new Event that occurs if at least one of the Events in the list occurs. If multiple occur at the same time they are folded from the left with the given function."

Finally, we need to convert your Dynamic t (Event t Int) into an Event t Int. We're going to use switch, which takes a Behavior t (Event t a) and returns an Event t a. So, the following line will result in Event t Int.

switch (current dynEv)

current extracts the Behavior of a Dynamic, and switch creates "an Event that will occur whenever the currently-selected input Event occurs."

Here's the revised listChoiceWidget code. I've included inline type annotations, so you'll need the ScopedTypeVariables language extension enabled to compile this code (or you can remove the annotations).

listChoiceWidget
  :: forall t m. MonadWidget t m
  => Dynamic t [String]
  -> m (Event t Int)
listChoiceWidget choices = el "div" $ do
  asMap <- mapDyn (Map.fromList . zip [(0::Int)..]) choices
  evs :: Dynamic t (Map.Map Int (Event t ())) <- listWithKey asMap (\_ s -> dynButton s)
  dynEv :: Dynamic t (Event t Int) <- mapDyn (leftmost . Map.elems . Map.mapWithKey (\k e -> fmap (const k) e)) evs
  return $ switch (current dynEv)

Here's a gist of the complete file.

like image 138
ali Avatar answered Nov 15 '22 09:11

ali