Closed geraldus closed 10 years ago
let eChan1 = map (const channel1) eButton1
Is it correct? Looks like map should take two events and return another event, but (const channel1)
is Behavior
, right?
Can you explain, what means mapping Event over Event?
I don't quite understand what you want to do. Do you want to make an event that captures the value of a behaviour when the button click happens? If so, use snapshot. Otherwise, explain a bit more what you want and I'll tell you how to do it.
There are opening "triggers" and one closing "trigger". The idea is to have one Behavior, which holds value of last clicked trigger, if it was closing button the value should be Nothing
, otherwise the value is Just PopupTab
. Thus, having such behavior we can listen for its values, and when new value occurs we can decide what to do: open specific tab or hide modal window.
Is it understandable?
P.S. In the example above I don't have listen for actual click events yet. The idea was to create inside IO event streams for each trigger (mkEvents
), then produce final behavior (makeReactiveUi
) and then bind reactive events to actual clicks.
Oh, I see. 'map' is for lists only. You want fmap.
let eChan1 = fmap (const (Just channel1)) eButton1
let eChan2 = fmap (const Nothing) eButton2
b <- hold Nothing (eChan1 <> eChan2)
Note that <> is shorthand for merge. It is imported from Data.Monoid.
Stephen, can you please explain what means to map a behavior over event? I want to catch the meaning. Oh, guess I understood. Let me check...
let eChan1 = map (const channel1) eButton1
let eChan2 = map (const channel2) eButton2
let sel = hold fuzz (merge eChan1 eChan2)
let screen = switch sel
In this case sel
should have the type Reactive (Behavior Image)
, but switch
needs Behavior (Behavior a))
.
Right?
UPDATE: Finally, I've solved type errors, and it compiles now. The next step is to bind reactive logic with actual event occurrences. When I'll finish everything I'll post my results with explanations and comments.
'map' should be 'fmap' and the third line should be
sel <- hold fuzz (merge eChan1 eChan2)
I don't know what it means to map a behaviour over an event. Are you talking about mapping a function over an event? That is what fmap does.
Yes, I've already guess that there should be fmap
, then I understood that const channel
is not a Behavior
, but function which returns a Behavior
. I've also used <-
instead of let =
with sel
.
Thank you anyway.
Now I have a little issue about how to bind actual events without JQuery and how to call some IO actions when events occur, but I assume the one of possible solutions it to use MVar
s (like JQuery does), so I studying this theory now.
MVars and forkIO are a good way to do things. In GHC and GHCJS they are very light-weight, so you should use them whenever you need them.
The problem is the same, I have not use them yet, so I need some time to understand what is it and how all it works. :) I only have read "Learn You a Haskell For a Great Good". Thus I still can do very limited things, but I hope soon things will become more familiar for me.
UPDATE: Cool! I've understood how MVars works, what forkIO is for and how event listening implemented in ghcjs-jquery. Heh, it took me a while. Now I have full code which compiles, but without rendering yet! Now I testing it, and I see within inspector event listeners on DOM elements but after clicks happens nothing, I'll find out an issue and post the first unrefactored working version with explanations.
Sthephen, I need your help. I've done some tests which shown that code works. But there is one issue, let me show. Please have a look at code:
-- ...
eventListener :: DomElement
-> String
-> (JSEvent -> IO())
-> IO ()
eventListener el et hnd = do
mv <- newEmptyMVar :: IO (MVar JSEvent)
forkIO (forever $ takeMVar mv >>= hnd)
js_addEventListener el (toJSString et) (mvarRef mv)
mkEvents :: Int -> IO [(Event a, a -> Reactive ())]
mkEvents n = liftM (replicate n) $ sync newEvent
initUi :: IO ()
initUi = do
w <- bldWidget
es <- mkEvents $ length $ tabs w
rui <- sync $ mkReactiveUi w es
let actions = map (\epr -> (\_ -> sync $ (snd epr) ())) es
mapM_ (\(a,t) -> do
eventListener (exTrigger t) "click" a
eventListener (inTrigger t) "click" a
) $ zip actions $ tabs w
kill <- sync $ listen (values rui) test
-- ...
First I make a list of pairs of events and their pushing functions. Then I create reactive logic. Finally, I make a list of functions which should be called when real click events occurs, namely actions
. The type of each action is (JSEvent -> IO ())
, the idea is when event occurs I push unit value ()
to concrete event stream. But this is not working.
I tried to replace action function with simple putStrLn
actions and it works, when I click trigger I have a message in console. Thus reactive logic works. E.g.
let actions = map (\epr -> (\_ -> putStrLn "Yeeep!")) es
If I call some action inside main
function, the logic works too and test
function calls.
One thing I can guess that sync $ pushE ()
does not have effect, because it runs inside forked thread. What can you say about this?
I have read the code but I can't see a specific problem. Can you tell me what mvarRef and test do?
The most likely thing is that there is a deadlock, because there is a mutex (implemented using MVar) around Sodium execution. The only rule is that you can't use 'sync' inside a 'listen' handler. However, if the listener handler blocks, that wouldn't work either.
Here are some suggestions: Try using a Chan instead of an MVar. An MVar can block on write but a Chan can't.
Or, instead of (mvarRef mv) - which I assume means (putMVar mv), use (\ev -> forkIO $ hnd ev).
mvarRef
takes MVar JSEvent
and returns JSObject (MVar JSEvent)
. It's just unsafeCoerce
behind the scenes. I think this is needed to have ability pass MVar itself to javascript code. Test now just prints "Nothing" or "Click" depending on arrived value (of type Maybe PopupTab
), in final example it will be replaced with rendering function.
UPDATE I've tested my code with JQuery event listeners and discovered that it is not work also. I used this handler:
c <- select "#ppp-close"
click (\_ -> putStrLn "!" >> S.sync ((snd (head es)) ()) ) def c
When I click on "close button" "!"
appears in console, but reactive logic misses sync action.
So, now I know that's not my flaw. Let me study about Chan.
Stephen, can you help with Chans? When I used MVars I used built-in shim h$makeMVarListener
to create javascript code which writes values to MVar of new occurred events. And I used another thread created with forkIO
to read that values from MVar.
Now I can't guess how write values of events to channel from Javascript. Moreover if my assumption about that sync action will have no effect inside another thread means that I will have to implement some logic (e.g. forever loop) which checks each channel for presence of new values.
In other words I need some kind of example how to use Chan
and populate it with values from JS.
In Haskell you would replace putMVar with writeChan. Can you show me the code where you write into the MVar?
I'll tell you what would be a really easy way to do it: forkIO a new thread that reads from your MVar and writes into your Chan. That thread can't block, so the MVar then can't block on write.
Here it is:
foreign import javascript unsafe "$1.addEventListener($2, h$makeMVarListener($3, false, false, false));"
js_addEventListener :: DomElement -> JSString -> JSObject (MVar JSEvent) -> IO ()
eventListener :: DomElement
-> String
-> (JSEvent -> IO())
-> IO ()
eventListener el et hnd = do
-- Create empty MVar
mv <- newEmptyMVar :: IO (MVar JSEvent)
-- Loop in separate thread that reads new values and passes them to handler function
forkIO (forever $ takeMVar mv >>= hnd)
-- Create javascript code, which will write new values to MVar when even occurs.
js_addEventListener el (toJSString et) (mvarRef mv)
initUi :: IO ()
initUi = do
w <- bldWidget
es <- mkEvents $ length (tabs w) + 1
rui <- sync $ mkReactiveUi w es
kill <- sync $ listen (values rui) render
let pushClose = snd (head es)
callback <- asyncCallback False ((putStrLn "X") >> (sync $ pushClose ()))
putStrLn "Initialized!"
kill
Few hours ago happened something strange and I've log big piece of code. Writing value to MVar is done by using shim h$makeMVarListener
, I use it in js_addEventListener
. The code I've presented prints to console "X"
, but render function does not evaluated, I assume there is no lock, because each event is handled, only sync operation have no effect.
The first thing that I've tried was to fork a new thread which reads MVar and writes its value to Chan, but have no luck. The one thing I've did not tested yet is not to fork new thread, but create forever loop, which reads values from MVar/Chan.
P.S. There are no listeners for clicks on all triggers (because of this code lost due bug) in last code example now, but here I've tested built-in way to create callbacks and passing this callbacks directly to Javascript. Here is the missing piece with testListener
:
foreign import javascript unsafe "$1.addEventListener($2, $3);"
js_testListener :: DomElement -> JSString -> (JSFun (IO ())) -> IO ()
testListener :: DomElement -> String -> (JSFun (IO ())) -> IO ()
testListener el et cb = do
js_testListener el (toJSString et) cb
Earlier clicks were listened this way:
let actions = map (\epr -> (\_ -> sync $ (snd epr) ())) es
mapM_ (\(a,t) -> do
eventListener (exTrigger t) "click" a
eventListener (inTrigger t) "click" a
) $ zip actions $ tabs w
kill <- sync $ listen (values rui) test
In all cases events are handled, the only problem is that sync $ pushE ()
have no effect.
I have no idea what's wrong.
eventListener' :: DomElement -> String -> IO (MVar JSEvent)
eventListener' de et = do
mv <- newEmptyMVar :: IO (MVar JSEvent)
js_addEventListener de (toJSString et) (mvarRef mv)
return mv
initUI :: IO ()
initUI = do
-- ...
mv <- eventListener' (closeB w) "click"
putStrLn "Initialized!"
kill
forever $ tryTakeMVar mv >>= run (\_ -> putStrLn "X" >> (sync $ pushClose ()))
where run :: (JSEvent -> IO ()) -> Maybe JSEvent -> IO ()
run _ Nothing = return ()
run h (Just e) = h e
This code gives the same result, it successfully prints "X" to console, but reactive sync action have no effect, even though it executes inside main thread within forever loop.
I also tried to use Chans:
foreign import javascript unsafe "$1.addEventListener($2, $3);"
js_testListener1 :: DomElement -> JSString -> (JSFun (JSEvent -> IO ())) -> IO ()
chanListener :: DomElement -> String -> (JSEvent -> IO ()) -> IO ()
chanListener el et cb = do
ch <- newChan :: IO (Chan JSEvent)
forkIO (forever $ readChan ch >>= cb)
callback <- syncCallback1 False True (\e -> writeChan ch e)
js_testListener1 el (toJSString et) callback
Javascript writes new value to Chan itself. Event listener created inside initUi
this way chanListener (closeB w) "click" (\_ -> putStrLn "X" >> (sync $ pushClose ()))
.
This gives the same results.
So, where is the trouble lies?
Kawaboonga! I found out the reason! When I removed kill
function within initUi
everything works now! Look:
initUi :: IO ()
initUi = do
w <- bldWidget
es <- mkEvents $ length (tabs w) + 1
rui <- sync $ mkReactiveUi w es
kill <- sync $ listen (values rui) render
let pushClose = snd (head es)
mv <- eventListener' (closeB w) "click"
putStrLn "Initialized!"
-- | If uncomment next line reactive logic stops to work!
-- kill
forever $ tryTakeMVar mv >>= run (\_ -> putStrLn "X" >> (sync $ pushClose ()))
where run :: (JSEvent -> IO ()) -> Maybe JSEvent -> IO ()
run _ Nothing = return ()
run h (Just e) = h e
Stephen, can you explain why this happens? I wrote kill
there to be sure that it will not be garbage collected. Meanwhile I'll finish my example.
I am actually not entirely sure whether 'kill' is needed or not. If it is called, it will cancel the listener, so if your program loops forever, what you are trying to do is to keep the 'kill' alive, but not run it.
I think I may have been completely wrong about this. When I find some time, I will look at this more and tidy it up. In the meantime, if it works without kill, then I think you are good.
Cool! I've just tested all techniques of click handling and all of them (MVar, Chan and JSFun) works now!
Sorry I didn't help much. I have been really busy today. Will be away from keyboard over the weekend.
While you're here I'll ask one little question.
Looks like my idea about merging events via foldl
don't work:
mkReactiveUi pppw es = do
let closeBhv = pure (Nothing :: Maybe PopupTab)
items' <- mapM (\t -> newBehavior (Just t) >>= return . fst) $ tabs pppw
let items = closeBhv:items'
events <- mapM (return . fst) es
let eItems = zipWith (\i e -> fmap (const i) e) items events
sel <- hold closeBhv $ mergeFold eItems
screen <- switch sel
return screen
where mergeFold :: [Event a] -> Event a
mergeFold (e:[]) = e
mergeFold (e:es) = foldl merge e es
es
is events of type Event (Behavior Maybe PopupTab)
each for concrete tab and one for close button.
I believed that if I merge them into one event using foldl
then click on any trigger will produce an event, containing specific Behavior. But in real world no matter what trigger was clicked, it always return the same value.
Have a nice weekend!
"foldl1' merge es" is shorter (import from Data.List)
That looks OK to me and should work. If it's really broken, turn it into a test case and I'll fix it.
I've finally updated my first post, it took me a week to write first edition of tutorial. Please have a look.
That's great! I've put a link to it on my blog: http://blog.reactiveprogramming.org/?p=151
very cool, I'll read it tonight and see if I have suggestions.
There are going to be some new options for JS event handling soon, that make it easier to write code that does not leak memory when removing DOM nodes while still behaving as expected with weak references (which Sodium uses to clean up its FRP graph)
(With an mvarListener
like ghcjs-jquery
you get a listener thread that's never deallocated (unless you throwTo
an async exception to it, but the current version doesn't give you the ThreadId
required for that. This is far from ideal, so I'll probably change this in ghcjs-jquery
before uploading the first version to hackage).
@luite, hi! To compare two DOM elements I've used foreign javascript code in my tutorial:
foreign import javascript unsafe "$1 == $2"
js_eq_elements :: DomElement -> DomElement -> IO Bool
I've tried to derive Eq
instance for my DomElement
type, but faced compilation errors. I was unable to solve this issue myself. Is there a way to have possibility to compare custom data types directly in Haskell?
I've also going to place working demo of this code somewhere.
Can't you use the eqRef
function in GHCJS.Types
? Since you're comparing references the comparison should be pure.
I have updated the ghcjs-base
code a bit to make it clearer how callbacks are retained by foreign code (beware that DOM retention doesn't actually work yet), but it does change the types.
Also there are some other GHCJS updates to fix some sodium related bugs, and the pull req I sent to the sodium repository removes the reliance of sodium on unpredictable behaviour of finalizers on regular Haskell objects (GHCJS behaves a bit different most of the time, but according to the specs is still correct)
Thanks for the tutorial geraldus. I actually couldn't get this code working with current ghcjs. So I changed it to use ghcjs-jquery
which also simplifies things...
module Main where
import Control.Applicative( pure )
import Control.Concurrent( forkIO )
import Control.Concurrent.Chan
import Control.Concurrent.MVar
import Control.Monad( foldM, forever, liftM, mapM, replicateM, when, void, join )
import Data.Default
import qualified Data.Text as T
import GHCJS.Foreign
import GHCJS.Types
import FRP.Sodium
import JavaScript.JQuery hiding (Event)
import qualified JavaScript.JQuery as JQ
data PopupTab = PopupTab { exTrigger :: JQuery
, inTrigger :: JQuery
, content :: JQuery
}
data PopupWidget = PopupWidget { window :: JQuery
, closeB :: JQuery
, tabs :: [PopupTab]
}
-- | Assert presence of ui elements.
asserted :: IO Bool
asserted = return True
foreign import javascript unsafe "$1.length" jq_length :: JQuery -> IO Int
elems :: JQuery -> IO [JQuery]
elems a = do
len <- jq_length a
mapM (flip eq a) [0..len - 1]
bldWidget :: IO PopupWidget
bldWidget = do
pppWnd <- select "#popup"
pppCls <- select "#ppp-close"
tabs <- find ".content" pppWnd >>= elems
pppTbs <- gatherTabs tabs
return $ PopupWidget pppWnd pppCls pppTbs
where gatherTabs :: [JQuery] -> IO [PopupTab]
gatherTabs = mapM (\t -> getAttr "id" t >>= bldTab . (T.drop 8))
bldTab eId = do
exT <- select (T.append "#ppp-show-" eId)
inT <- select (T.append "#item-" eId)
cnt <- select (T.append "#content-" eId)
return $ PopupTab exT inT cnt
mkEvents :: Int -> IO [(Event a, a -> Reactive ())]
mkEvents n = sync $ replicateM n newEvent
mkReactiveUi :: PopupWidget
-> [(Event a, a -> Reactive ())]
-> Reactive (Behavior (Maybe PopupTab))
mkReactiveUi pppw es = do
let closeBhv = pure (Nothing :: Maybe PopupTab)
items' <- mapM (\t -> newBehavior (Just t) >>= return . fst) $ tabs pppw
let items = closeBhv:items'
events <- mapM (return . fst) es
let eItems = zipWith (\i e -> fmap (const i) e) items events
sel <- hold closeBhv $ mergeFold eItems
switch sel
where mergeFold :: [Event a] -> Event a
mergeFold (e:es) = foldl merge e es
render :: PopupWidget -> Maybe PopupTab -> IO ()
render w Nothing = (popupClose w) >> (releaseTabs w)
render w (Just t) = (popupOpen w) >> (renderTabs w t)
popupClose :: PopupWidget -> IO ()
popupClose w = void $ addClass "Dn" (window w)
popupOpen :: PopupWidget -> IO ()
popupOpen w = void $ removeClass "Dn" (window w)
releaseTabs :: PopupWidget -> IO ()
releaseTabs w = mapM_ (\t -> do
removeClass "hold" (exTrigger t)
removeClass "hold" (inTrigger t)
) (tabs w)
renderTabs :: PopupWidget -> PopupTab -> IO ()
renderTabs w t = mapM_ (\t' -> do
cid' <- getAttr "id" (content t')
cid <- getAttr "id" (content t)
if cid == cid'
then do
removeClass "Dn" (content t')
addClass "hold" (exTrigger t')
addClass "hold" (inTrigger t')
else do
addClass "Dn" (content t')
removeClass "hold" (exTrigger t')
removeClass "hold" (inTrigger t')
) (tabs w)
initUi :: IO ()
initUi = do
w <- bldWidget
es <- mkEvents $ length (tabs w) + 1
rui <- sync $ mkReactiveUi w es
kill <- sync $ listen (value rui) (render w)
mapM_ (\(t,(_,f)) -> do
click (const $ sync $ f ()) def (exTrigger t)
click (const $ sync $ f ()) def (inTrigger t)
) $ zip (tabs w) (tail es)
let pushClose = snd (head es)
click (\_ -> sync $ pushClose ()) def (closeB w)
putStrLn "Initialized!"
main :: IO ()
main = asserted >>= flip when initUi
Thanks! Hope, this was helpful. Yes, there was some updates, I've planned to update example code, but haven't done it. I've used jQuery a lot in past. But then I started to use exciting Polymer project and faced an issue: using both jQuery and Polymer libraries was impossible because of Polymer assertion error. Because of it I weaned myself from jQuery :) I plan to introduce something similar to Polymer based on functional reactive approach with Sodium and GHCJS, but unfortunately my learning and coding temps are similar to snail's speed.
Rewritten at 2nd of March, 2014
Authors preface
This is first the edition of my tutorial. Please excuse me my poor English. I'll be happy if you can help make my text more readable. Also the code itself could be affected by imperative style. Please leave your comments and recommendations if you want.
This tutorial shows:
merge
andswitch
primitives;Overview
We'll see a simple web application, which consist of single HTML page. There are page header, some content, hidden popup window and triggers which opens that popup.
When you click a trigger popup appears. There are duplicated triggers and close button inside popup.
To design UI logics we'll use Sodium. The idea adopted from Sodium's presentation:
In our case javascript click events on triggers represent button events, selected tabs represent channels and closed popup represents the fuzz.
Data Types
Let's design data types:
PopupTab
consist of external trigger (which is outside the popup window), internal trigger and element with content.PopupWidget
is consist ofwindow
which is widget itself,closeB
which is just a quick reference to button that closes popup, andtabs
which is a quick reference to widget tabs.Here is HTML code of widget:
Thus, we need a list of evens with pushing functions, a list of behaviors of PopupTab and our fuzz analogue. We'll use
Maybe PopupTab
type to represent our channels and fuzz, e.g. for closed popup window the value would beNothing
and for selected tab it will beJust
value with concrete tab inside.We'll split our final goal into four tasks:
Reactive Logic
To separate reactive code from IO let's design a function which takes a list of events, its pushing functions and popup widget and which returns a behavior of type
Behavior (Maybe PopupTab)
. Then we'll listen value of that behavior and render our UI depending on its value. Sodium'snewEvent
function returns an event paired with it's pushing function it's so we'll use one list of pairs, e.g.[(Event a, a -> Reactive ())]
. Here is our reactive code (please read comments):So, we have a click events (of type
Event ()
). Our reactive logic will work this way:Great! Now we have the first piece!
Finding DOM Elements
This task is quite easy, and I think detailed explanations are unnecessarily. (However if you'll ask I can explain it a bit.) The code is quite obvious itself:
One thing I should mention about is that we are ignoring possible failures when searching elements on page, assuming that all target elements are presented on page for sure (for sake of simplicity of example).
Javascript Events Handling
So, now we have two tasks remaining: to bind actual clicks with reactive logic and render changes. There are few techniques to handle javascript events. Each technique requires to import foreign javascript function, which actually can bind some handler function to event occurrences, the difference is in what to use as a handler and how to use it. Let's take a quick look at each of them.
First possibility: using GHCJS' javascript callback primitives. The idea is to send to javascript as a handler Haskell function of type
JSFun a
, which is Haskell IO code wrapped by JSFun. We can create such functions using following GHCJS primitives:asyncCallback
asyncCallback1
asyncCallback2
syncCallback
syncCallback1
syncCallback2
These functions names are pretty self explaining, functions without a tailing number in name are functions that take no arguments; tailing
1
means that functions take one argument and2
means functions that take 2 arguments. Sync callbacks takes two boolean arguments and Haskell function which should return a value of typeIO a
, and async callbacks takes one boolean argument and Haskell function. For now I can't explain what these boolean arguments mean.Here is an example:
The second opportunity is to use GHCJS shim, which provides us javascript function
h$makeMVarListener()
. This functions takes 4 arguments, first is a reference toMVar
, last three are to configure event's propagation, event's immediate propagation and event's default behavior (you can look at its source code yourself). This technique is used in GHCJS-JQuery.In this case instead of passing Haskell code directly to javascript, we creating an
MVar
first, which will hold javascript events.h$makeMVarListener
produces function, which are bound as a handler to event occurrences. When event will happen javascript will write new event value to MVar. All we need is to monitor that MVar and read values when they arrives. This could be done usingforkIO
. Here is an example:And the last opportunity is combination of previous techniques:
MVar
orChan
holder for event values,Here is an example with
Chan
:In our final code we'll use the second technique.
Rendering Changes
So, we have a reactive behavior of type
Behavior (Maybe PopupTab)
which we will listen for values. Remember thatJust
values represent opened tab andNothing
value represents closed popup window. When tab is selected both its triggers should be marked. We'll mark them by class.hold
. We should make popup window and correspondingdiv
block to be visible. We'll control visibility by changingdisplay
property. Thus, if value isNothing
we should remove all marks from triggers and make popup hidden, in all other cases we should make popup visible, mark corresponding triggers, and make proper content to be visible.Here is the code:
Great! Looks like have all pieces of puzzle now!
Full Code
Here is the final Haskell code:
Markup:
Styles: