% Real World Reflex % Doug Beardsley % Takt
Audience
- People with prior reflex knowledge
- Interested in design patterns for larger apps
My Experience
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
Goal
- Overall, building UIs with Reflex is fantastic.
- This talk describes some less obvious things encountered while building real world app.
Outline
- The API
- Existence and Maybe
- Widget Patterns
- FRP Frames
- Causality Loops
You will forget this
I've given this talk to multiple people as onboarding and they all came back
with questions later that were answered here.
Come back to it
- Don't feel bad
Note on code examples
Correct type signature
foo :: MonadWidget t m => Dynamic t Foo -> m (Event t ())
In this presentation
foo :: Dynamic Foo -> m (Event ())
The API
Event, Behavior, Dynamic
- 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
The Big Question
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
It really is exactly that
Yes, but that doesn't help
- 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
Event and 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
Dynamic
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)
Dynamic 2
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
Instances
Functor | Applicative | Monad | |
---|---|---|---|
Event | X | ||
Behavior | X | X | X |
Dynamic | X | X | X |
T.pack . show <$> anEvent
() <$ anEvent
Existence and Maybe
Must have a value, but don't have one?
hold :: a -> Event a -> m (Behavior a)
holdDyn :: a -> Event a -> m (Dynamic a)
Use the adjunction!
(a Maybe)
s/a/Maybe a/
- How do you go the other way?
maybe
,fromMaybe
, etc- We have one more tool:
Use the adjunction!
(a Maybe)
fmapMaybe :: (a -> Maybe b) -> Event a -> Event b
fmapMaybe id :: Event (Maybe a) -> Event a
Widget Patterns
textErr
How to write?
textErr :: Either Text Text -> m ()
textErr
textErr :: Either Text Text -> m ()
textErr (Left err) = elClass "span" "red-text" $ text err
textErr (Right t) = text t
dynTextErr
- How to write?
- dynTextErr :: Dynamic (Either Text Text) -> m ()
- Can't pattern match
Higher-order FRP
- Reflex has two core higher-order functions:
- widgetHold :: m a -> Event (m a) -> m (Dynamic a)
- dyn :: Dynamic (m a) -> m (Event a)
Nested Reactive Values
| Outer | Inner | Collapsing Function |
|----------|----------|---|
| Dynamic | Dynamic | |
| Dynamic | Behavior | |
| Dynamic | Event | |
| Behavior | Dynamic | |
| Behavior | Behavior | |
| Behavior | Event | |
| Event | Dynamic | |
| Event | Behavior | |
| Event | Event | |
Nested Reactive Values
| 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 |
Input Widgets
"Definitive" widget
widget :: a -> Event a -> m (Dynamic a)
View Widgets
widget :: Dynamic a -> m (Event a)
FRP Frames
Frames
- 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
Frames and Dynamics
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
Causality Loops
Recursive Do
tweetWidget = do
click <- button "Send Tweet"
ta <- textArea $ def & setValue .~ ("" <$ click)
...
vs
tweetWidget = do
rec ta <- textArea $ def & setValue .~ ("" <$ click)
click <- button "Send Tweet"
...
Loops
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.
Loop Symptoms
- 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?
Finding Loops 1
- Loops can only happen in recursive structures.
- In practice, this is almost always a
rec
block. - So keep them as small as possible.
Minimizing the rec
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]
Finding Loops 2
- Loops can only happen in a
rec
block, so keep them as small as possible - Disable whole blocks of code
Substitutions 1
For
m ()
Replace with
return ()
Substitutions 2
For
m (Event a)
Replace with
return never
Substitutions 3
For
m (Dynamic (Maybe a))
Replace with
return (constDyn Nothing)
Finding Loops 3
- Loops can only happen in a
rec
block, so keep them as small as possible - Disable whole blocks of code
- Binary search
Binary Search
val <- case ty of
InputChar -> charWidget
InputString -> stringWidget
InputInt -> intWidget
InputDouble -> doubleWidget
InputDate -> dateWidget
InputTime -> timeWidget
InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
Binary Search
val <- case ty of
-- InputChar -> charWidget
-- InputString -> stringWidget
-- InputInt -> intWidget
-- InputDouble -> doubleWidget
InputDate -> dateWidget
InputTime -> timeWidget
InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
Binary Search
val <- case ty of
InputChar -> charWidget
InputString -> stringWidget
InputInt -> intWidget
InputDouble -> doubleWidget
-- InputDate -> dateWidget
-- InputTime -> timeWidget
-- InputDateTime -> dateTimeWidget
-- InputEnum -> enumWidget
Binary Search
val <- case ty of
-- InputChar -> charWidget
-- InputString -> stringWidget
-- InputInt -> intWidget
-- InputDouble -> doubleWidget
-- InputDate -> dateWidget
-- InputTime -> timeWidget
InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
Binary Search
val <- case ty of
-- InputChar -> charWidget
-- InputString -> stringWidget
-- InputInt -> intWidget
-- InputDouble -> doubleWidget
-- InputDate -> dateWidget
-- InputTime -> timeWidget
-- InputDateTime -> dateTimeWidget
InputEnum -> enumWidget
Fixing Loops
- Use
current
- Avoid using "promptly" functions unless you know you need them
Fixing Loops: current
Two examples I've actually encountered
attachPromptlyDynWith func d e
attachWith func (current d) e
and
tagPromptlyDyn d e
tag (current d) e
Fixing Loops
- 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)
Recap
- Reflex is the most enjoyable UI development experience I've encountered.
- Hopefully these tips will help you get up and running more quickly.
Questions?
http://mightybyte.net/real-world-reflex/index.html
Looking for a job? Takt is hiring!