% Real World Reflex % Doug Beardsley % Takt
- People with prior reflex knowledge
- Interested in design patterns for larger apps
Two production reflex apps
- 02/2015 - 12/2015: 14,000 lines (Soostone)
- 10/2016 - present: 21,000 lines (Takt)
Misc small projects
- hsnippet.com
- github.com/reflex-frp/reflex-dom-contrib
- github.com/reflex-frp/reflex-dom-ace
- github.com/reflex-frp/reflex-dom-semui
- github.com/TaktInc/reflex-dhtmlx
- Overall, building UIs with Reflex is fantastic.
- This talk describes some less obvious things encountered while building real world app.
- The API
- Existence and Maybe
- Widget Patterns
- FRP Frames
- Causality Loops
I've given this talk to multiple people as onboarding and they all came back
with questions later that were answered here.
- Don't feel bad
Correct type signature
foo :: MonadWidget t m => Dynamic t Foo -> m (Event t ())
In this presentation
foo :: Dynamic Foo -> m (Event ())
- Event - Notifies you when something occurs (push)
- Behavior - Has a value at EVERY point in time (pull)
- Dynamic - Gives you both
Hard to know when to use which type
When to use each one?
- If you need to know when something happens...use Event
- If you need to be able to get a value at arbitrary points in time...use Behavior
- If you need to see something's value at arbitrary times AND know when it changes...use Dynamic
- DOM API is primarily push-oriented
- How would you implement
dynText :: Dynamic Text -> m ()
? - Need to know when it changes
- Need to be able to query the value in case you have to redraw
- Dynamic is usually preferred over Behavior
never :: Event a
hold :: a -> Event a -> m (Behavior a)
tag :: Behavior a -> Event b -> Event a
attach :: Behavior a -> Event b -> Event (a,b)
attachWith :: (a -> b -> c) -> Behavior a -> Event b -> Event c
switch :: Behavior (Event a) -> Event a
Accessors
current :: Dynamic a -> Behavior a
updated :: Dynamic a -> Event a
Construction
constDyn :: a -> Dynamic a
holdDyn :: a -> Event a -> m (Dynamic a)
foldDyn :: (a -> b -> b) -> b -> Event a -> m (Dynamic b)
tagDyn :: Dynamic a -> Event b -> Event a
attachDyn :: Dynamic a -> Event b -> Event (a,b)
attachDynWith :: (a -> b -> c) -> Dynamic a -> Event b -> Event c
Functor | Applicative | Monad | |
---|---|---|---|
Event | X | ||
Behavior | X | X | X |
Dynamic | X | X | X |
T.pack . show <$> anEvent
() <$ anEvent
hold :: a -> Event a -> m (Behavior a)
holdDyn :: a -> Event a -> m (Dynamic a)
s/a/Maybe a/
- How do you go the other way?
maybe
,fromMaybe
, etc- We have one more tool:
fmapMaybe :: (a -> Maybe b) -> Event a -> Event b
fmapMaybe id :: Event (Maybe a) -> Event a
How to write?
textErr :: Either Text Text -> m ()
textErr :: Either Text Text -> m ()
textErr (Left err) = elClass "span" "red-text" $ text err
textErr (Right t) = text t
- How to write?
- dynTextErr :: Dynamic (Either Text Text) -> m ()
- Can't pattern match
- Reflex has two core higher-order functions:
- widgetHold :: m a -> Event (m a) -> m (Dynamic a)
- dyn :: Dynamic (m a) -> m (Event a)
| Outer | Inner | Collapsing Function |
|----------|----------|---|
| Dynamic | Dynamic | |
| Dynamic | Behavior | |
| Dynamic | Event | |
| Behavior | Dynamic | |
| Behavior | Behavior | |
| Behavior | Event | |
| Event | Dynamic | |
| Event | Behavior | |
| Event | Event | |
| Outer | Inner | Collapsing Function |
|----------|----------|---|
| Dynamic | Dynamic | join |
| Dynamic | Behavior | join . current |
| Dynamic | Event | switch . current, switchPromptlyDyn |
| Behavior | Dynamic | join . fmap current |
| Behavior | Behavior | join |
| Behavior | Event | switch |
| Event | Dynamic | join . holdDyn iv |
| Event | Behavior | join . hold iv |
| Event | Event | switch . hold iv, switchPromptly, switchPromptOnly |
"Definitive" widget
widget :: a -> Event a -> m (Dynamic a)
View Widgets
widget :: Dynamic a -> m (Event a)
- A frame is the atomic time unit
- Frame begins with, say, a mouse click
- Mouse click event fires
- Events fmapped from that event fire
- All other events depending on those events fire
- Repeat until there are no more event firings
- Frame ends
current :: Dynamic a -> Behavior a
updated :: Dynamic a -> Event a
updated
gets the new value from the firing eventcurrent
gets the value from the previous frameuniqDyn
supresses the firing if nothing changed
tweetWidget = do
click <- button "Send Tweet"
ta <- textArea $ def & setValue .~ ("" <$ click)
...
vs
tweetWidget = do
rec ta <- textArea $ def & setValue .~ ("" <$ click)
click <- button "Send Tweet"
...
loopWidget = do
rec click <- button "Add"
val <- formWidget a
let a = leftmost [42 <$ click, updated val]
- They don't happen very often
- But when they do, it's hard to know how to debug them.
- App just hangs
- Hangs and consumes 100% CPU
- Throws bizarre error message
- "heightBagRemove: Height 20 not present in bag HeightBag..."
- "Causality loop found"
- something else?
- Loops can only happen in recursive structures.
- In practice, this is almost always a
rec
block. - So keep them as small as possible.
loopWidget = do
rec click <- button "Add"
val <- formWidget a
let a = leftmost [42 <$ click, updated val]
vs
loopWidget = do
click <- button "Add"
rec val <- formWidget a
let a = leftmost [42 <$ click, updated val]
- Loops can only happen in a
rec
block, so keep them as small as possible - Disable whole blocks of code
For
m ()
Replace with
return ()
For
m (Event a)
Replace with
return never
For
m (Dynamic (Maybe a))
Replace with
return (constDyn Nothing)
- Loops can only happen in a
rec
block, so keep them as small as possible - Disable whole blocks of code
- Binary search
val <- case ty of
InputChar -> charWidget
InputString -> stringWidget
InputInt -> intWidget
InputDouble -> doubleWidget
InputDate -> dateWidget
InputTime -> timeWidget
InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
val <- case ty of
-- InputChar -> charWidget
-- InputString -> stringWidget
-- InputInt -> intWidget
-- InputDouble -> doubleWidget
InputDate -> dateWidget
InputTime -> timeWidget
InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
val <- case ty of
InputChar -> charWidget
InputString -> stringWidget
InputInt -> intWidget
InputDouble -> doubleWidget
-- InputDate -> dateWidget
-- InputTime -> timeWidget
-- InputDateTime -> dateTimeWidget
-- InputEnum -> enumWidget
val <- case ty of
-- InputChar -> charWidget
-- InputString -> stringWidget
-- InputInt -> intWidget
-- InputDouble -> doubleWidget
-- InputDate -> dateWidget
-- InputTime -> timeWidget
InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
val <- case ty of
-- InputChar -> charWidget
-- InputString -> stringWidget
-- InputInt -> intWidget
-- InputDouble -> doubleWidget
-- InputDate -> dateWidget
-- InputTime -> timeWidget
-- InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
- Use
current
- Avoid using "promptly" functions unless you know you need them
Two examples I've actually encountered
attachPromptlyDynWith func d e
attachWith func (current d) e
and
tagPromptlyDyn d e
tag (current d) e
- Use
current
- Use input widget "change" events instead of
value
- Things must be sufficiently lazy
- Only pass Event, Behavior, and Dynamic up a rec
- Don't passs, e.g. (Event, Event)
- Reflex is the most enjoyable UI development experience I've encountered.
- Hopefully these tips will help you get up and running more quickly.
http://mightybyte.net/real-world-reflex/index.html
Looking for a job? Takt is hiring!