'spread-sheet'-like programming
used for GUIs, robotics, music...
⇒ no need for mutation, handlers, callbacks
e.g. a Mouse click :: Event ()
e.g. the content of an input field :: Behavior String
document.getElementById("displayInput").value = "Not set";
function buttonClicked() {
var content = document.getElementById("sourceInput").value;
document.getElementById("displayInput").value = content;
}
<input id="sourceInput" type="text" />
<button onclick="buttonClicked()">Click me</button>
<input id="displayInput" type="text" />
bSource :: Behavior String
eClick :: Event ()
bDisplay :: Behavior String
bSource :: Behavior String, eClick :: Event ()
bDisplay :: Behavior String
bSource :: Behavior String
eClick :: Event ()
-- Combinators we find in Reactive.Threepenny
stepper :: MonadIO m => a -> Event a -> m (Behavior a)
(<@) :: Behavior a -> Event b -> Event a
-- Our solution
bDisplay = stepper "Not Set" (bSource <@ eClick)
-- Make displayInput show bDisplay
app :: Window -> UI ()
app w = void $ do
rec sourceInput <- UI.input
displayInput <- UI.input
clickBtn <- UI.button #+ [ string "Click Me" ]
let eClick :: Event () = UI.click clickBtn
sourceB <- stepper "" . UI.valueChange $ sourceInput
displayB <- stepper "Not Set" (sourceB <@ eClick)
element displayInput # sink value displayB
getBody w # set children [getElement sourceInput, getElement clickBtn
, getElement displayInput]
accumB :: MonadIO m => a -> Event (a -> a) -> m (Behavior a)
accumE :: MonadIO m => a -> Event (a -> a) -> m (Event a)
unions :: [Event a] -> Event [a]
apply :: Behavior (a -> b) -> Event a -> Event b
stepper :: MonadIO m => a -> Event a -> m (Behavior a)
unionWith :: (a -> a -> a) -> Event a -> Event a -> Event a
... a few more (see the the documentation)
type Key = Int
type Names = (String,String)
type Database = Map Key Names
let bDatabase :: Behavior Database
bDatabase = accumB' empty solution
solution :: [Event (Database -> Database)]
solution = [ _createName, _updateName, _deleteName ]
eCreate :: Event ()
eDelete :: Event ()
eNames :: Event Names
bSelection :: Behavior (Maybe Key)
create :: Names -> Database -> Database
update :: Maybe Key -> Names -> Maybe (Database -> Database)
delete :: Key -> Database -> Database
filterJust :: Event (Maybe a) -> Event a
(<@) :: Behavior a -> Event b -> Event a
(<@>) :: Behavior (a -> b) -> Event a -> Event b
createName :: Event (Database -> Database)
createName = ???
eCreate :: Event ()
emil = ("Emil,"Example") :: Names
create :: Names -> Database -> Database
const :: a -> b -> a
fmap :: (a -> b) -> Event a -> Event b
eCreate :: Event ()
emil = ("Emil,"Example") :: Names
create :: Names -> Database -> Database
const :: a -> b -> a
fmap :: (a -> b) -> Event a -> Event b
createEmil :: Database -> Database
createEmil = create emil
constCreateEmil :: () -> (Database -> Database)
constCreateEmil = const createEmil
createName :: Event (Database -> Database)
createName = fmap constCreateEmil eCreate
-- More succinct:
createName = create ("Emil","Example") <$ eCreate
deleteName :: Event (Database -> Database)
deleteName = ???
eDelete :: Event ()
bSelection :: Behavior (Maybe Key)
delete :: Key -> Database -> Database
fmap :: (a -> b) -> Event a -> Event b
(<@) :: Behavior a -> Event b -> Event a
filterJust :: Event (Maybe a) -> Event a
eDelete :: Event ()
bSelection :: Behavior (Maybe Key)
delete :: Key -> Database -> Database
fmap :: (a -> b) -> Event a -> Event b
(<@) :: Behavior a -> Event b -> Event a
filterJust :: Event (Maybe a) -> Event a
bSelectionOnDelete :: Event (Maybe Key)
bSelectionOnDelete = bSelection <@ eDelete
bKeyOnDelete :: Event Key
bKeyOnDelete = filterJust bSelectionOnDelete
deleteName :: Event (Database -> Database)
deleteName = fmap delete bKeyOnDelete
There is a repo of the Reflex author which provides a GHCJS environment with the Reflex library in a Nix sandbox
For build/dependency management of GHC projects there is stack. Does stack support GHCJS?
is explained in https://github.com/luigy/try-stack-reflex
stack upgrade --git
./try-stack-reflex ghcjsi # takes a long time!
stack ghci # Voilà!
⇒ Using GHC/GHCJS in a single project is going to be "magical"!
app = do
sourceInput <- textInput def
rec let config = def { _textInputConfig_initialValue = "Not Set" }
& setValue .~ (updated dDisplay)
displayInput <- textInput config
dDisplay <- holdDyn "" eContentWhenClicked
eClick <- button "Click Me"
let bSource = current $ _textInput_value sourceInput
eContentWhenClicked = tag bSource eClick
return ()
current :: Dynamic t a -> Behavior t a
updated :: Dynamic t a -> Event t a
tag :: Reflex t => Behavior t b -> Event t a -> Event t b
holdDyn :: MonadHold t m => a -> Event t a -> m (Dynamic t a)
mapDyn :: (Reflex t, MonadHold t m) =>
(a -> b) -> Dynamic t a -> m (Dynamic t b)
foldDyn :: (Reflex t, MonadHold t m, MonadFix m) =>
(a -> b -> b) -> b -> Event t a -> m (Dynamic t b)
attachDynWith :: Reflex t =>
(a -> b -> c) -> Dynamic t a -> Event t b -> Event t c
count :: (Reflex t, MonadHold t m, MonadFix m, Num b) =>
Event t a -> m (Dynamic t b)
...
foldDyn' :: x -> [Event t (x -> x)] -> Dynamic t x
cellContentCorrect :: (Event t Correctness, Event t Digit)
-> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = ?
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit) -> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = foldDyn' False [
-- should be True when the digit is a free digit
-- should be True when the digit cell input is correct
-- should be False when the cell input is not a digit
-- should be False when a new sudoku is chosen
-- should be False when the digit cell input is incorrect
]
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit) -> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = foldDyn' False [
const True <$ ffilter isFree eDigit
-- should be True when the digit cell input is correct
-- should be False when the cell input is not a digit
-- should be False when a new sudoku is chosen
-- should be False when the digit cell input is incorrect
]
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit) -> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = foldDyn' False [
const True <$ ffilter isFree eDigit
, const True <$ ffilter isCorrect eInput
-- should be False when the cell input is not a digit
-- should be False when a new sudoku is chosen
-- should be False when the digit cell input is incorrect
]
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit) -> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = foldDyn' False [
const True <$ ffilter isFree eDigit
, const True <$ ffilter isCorrect eInput
, const False <$ ffilter isNotADigit eInput
-- should be False when a new sudoku is chosen
-- should be False when the digit cell input is incorrect
]
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit) -> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = foldDyn' False [
const True <$ ffilter isFree eDigit
, const True <$ ffilter isCorrect eInput
, const False <$ ffilter isNotADigit eInput
, const False <$ eSelectedSudoku
-- should be False when the digit cell input is incorrect
]
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit) -> Dynamic t Bool
cellContentCorrect (eCorrectness, eDigit) = foldDyn' False [
const True <$ ffilter isFree eDigit
, const True <$ ffilter isCorrect eInput
, const False <$ ffilter isNotADigit eInput
, const False <$ eSelectedSudoku
, const False <$ ffilter isNotCorrect eInput
]
data Digit = Guess Int | Free Int
data Correctness = Correct | NotCorrect | NotADigit
isCorrect :: Correctness -> Bool
isNotADigit :: Correctness -> Bool
ffilter :: (a -> Bool) -> Event t a -> Event t a
eSelectedSudoku :: Event Int
cellContentCorrect :: (Event t Correctness, Event t Digit)
-> Dynamic t Bool
eCorrectness :: [Event t Correctness]
eDigits :: [Event t Digit]
mconcatDyn :: (Monoid a) => [Dynamic t a] -> Dynamic t a
dCorrectCells :: [Dynamic t Monoid.All] <-
forM (zip eCorrectness dDigits)
(isCellInput >=> mapDyn Monoid.All)
dSudokuSolved :: Dynamic t Bool <-
mconcatDyn dCorrectCells >>= mapDyn Monoid.getAll
elClass "h3" "solved" $ dynText =<< forDyn dSudokuSolved $
\isSolved -> if isSolved then "Solved!" else mempty