Vorlesung: Praxis der Funktionalen Programmierung | Index

FranTk-Beispiel: bpm-Zähler

An einem überschaubaren Beispiel zeige ich Widget-Komposition und Arbeit mit Ereignissen und Listenern.

Die Aufgabe ist, die Zeit zwischen zwei Mausclicks zu messen (und über die gemessenen Zeiten zu mitteln).

Zustands-Variablen

Wodurch ist ein Zustand des Interfaces gekennzeichnet? Jeden wichtigen Parameter merken wir uns in einer Verhaltens-Variable. Dafür steht im Programm
bpm :: Component
bpm = do
    first_click   <- mkBVar 0.0
    last_click    <- mkBVar 0.0
    clicks_so_far <- mkBVar 0.0
    average_bpm   <- mkBVar 0.0
    ...

Widgets und Verhalten

Dann, welche Widgets wir sehen wollen: Das sieht im Text so aus:
    let infos = mkLabel ...
    let reset = mkButton [ text "reset" ] ...
    let click = mkButton [ text "click" ] ...
...
    nbeside [ reset, infos, click ]
...
         let men = mkMenu [] 
                 [ mcascade [text "file"] $ 
                   mkMenu [] [mbutton [text "quit"] quit]
                 ] 
         withRootWindow [title "bpm counter", useMenu men] bpm
So sagen wir "das Label soll immer den aktuellen Wert der Variablen anzeigen":
    let infos = mkLabel 
	      [ font (Font "10x20")
	      , textB $ lift1 ( \ x -> "average bpm : " ++ show x )
		      $ bvarBehavior average_bpm
	      ] 
Jedesmal, wenn average_bpm geschrieben wird, zeichen FranTk das Label neu.

Ereignisse und Listener

Schließlich, welche Aktionen bei Clicks auf den Knöpfen stattfinden sollen. In FranTk programmieren wir das, indem wir für jeden Button einen Listener angeben. Buttons sind Erzeuger von Ereignissen, Listener sind Verbraucher.

Die einfachste Art von Listener ist ein input-Listener für eine BVar. Bei "reset" schicken wir z. B. eine 0.0 an den input-Listener von clicks_so_far. Wo kommt die 0.0 her? Vom Button bekommen wir ein Ereignis vom Typ Event (), wir brauchen aber Event Double. Wir müssen das mappen:

mapL ( \ () -> 0.0 ) ( input clicks_so_far )
Zur Übung: welchen Typ hat mapL? Offenbar
mapL :: (a -> b) -> (Listener b -> Listener a)
Das ist ein kontravarianter Funktor.

Im eben gezeigten Fall ist uns ja der Wert des ankommenden Ereignisses egal. Wichtig ist, daß wir daraus eine 0.0 machen. Dafür gibt es

tellL :: Listener a -> a -> Listener b
tellL l x = mapL (const x) l
Nun wollen wir nicht nur eine Variable updaten, sondern mehrere. Dazu müssen wir Listener parallelschalten. Das geht mit
mergeL :: Listener a -> Listener a -> Listener a
anyL   :: [ Listener a ] -> Listener a
Tatsächlich ist die Definition (siehe FranTk/src/FranSrc/Listener1.hs)
anyL = foldr mergeL neverL
Was neverL macht, können wir uns denken: nichts.

Snapshots

Wir wollen nun gern wissen, wann der User clickt (und nicht nur, daß er clickt). Woher kommt die aktuelle Zeit? Es gibt einen Wert time :: Behavior Double, dessen aktuellen Wert wollen wir dem Listener mitteilen. Dazu gibt es
snapshotL :: Behavior a -> Listener (b,a) -> Listener b
Wir benutzen das so:
timed :: Listener Time -> Listener ()
timed listener = snapshotL time $ mapL ( \ ((), t) -> t ) $ listener
Beachte wieder die Umkehrung der Typen.

Zusammenbau

Jetzt haben wir alles beisammen und programmieren
   let reset = mkButton [ text "reset" ]
	      ( timed
	      $ anyL [ input first_click
		     , input last_click
		     , tellL (input clicks_so_far) 0
		     , tellL (input average_bpm  ) 0
		     ] 
	      )
sowie die eigentliche Rechnung hier
    let click = mkButton [ text "click" ]
	      ( timed
	      $ anyL [ input last_click
		     , tellL (bvarUpdInput clicks_so_far) (1 +)
		     , snapshotL ( bvarBehavior first_click )
		     $ snapshotL ( bvarBehavior clicks_so_far )
		     $ mapL ( \ ( (t, f), c ) -> (c + 1) * 60 / (t - f) )
		     $ input average_bpm
		     ]
	      )
Hier ist der vollständige Quelltext.

Hausaufgabe: Zeigen die bmp-Zahl zusätzlich durch einen variabel langen Balken oder so etwas ähnliches an.


best viewed with any browser


http://www.informatik.uni-leipzig.de/~joe/ mailto:joe@informatik.uni-leipzig.de