This was originally posted under the NY-Haskell.org blog.
Today we learned how to get started with a Reflex and Reflex-Dom with Ali Abrar.
First, install Reflex platform.
git clone [email protected]:reflex-frp/reflex-platform.git
./try-reflex
During the installation we chatted with Ali and Ryan (creator of ReflexFRP) to understand more about the background of this library and how it compares to other open source tools available.
How does reflex compare to other frameworks?
There’s two parts: reflex, reflex-dom
Reflex gives frp functionalities. has datatypes (or building blocks) deals with interactive applications
Reflex-dom is a binding to web browsers api that is based on reflex. Takes callback driven, highly stateful thing that browser supports and turns it into reflex instead.
There are very few FRP dom specific comparisons for use in the browser. Elm is similar but not Haskell so you can’t use libraries found in Hackage.
How does reflex compare to reactjs?
Reactjs inspired approach is not purely frp, limited with inherent Javascript limitations.
The advantage of using ghcjs is that it gives you access to all of hackage. You can use any haskell library, and so with Reflex-Dom you have access on the front end to all the same stuff as backend applications.
Reflex itself can be used for a wide number of applications (websites, games, etc). Reflex-Dom can be used to produce javascript files. For example full stack web applications, or single web applications.
You can also use reflex-dom to create a native binary. Can produce web-gtk or similar variants, and also produce a distributable.
Documentation Resources
Here’s how you can look for documentation.
Basic Tutorial on compiling with ghcjs
After everyone installed reflex-platform, we began with coding.
Step 1 Create a blank file: workshop.hs
Step 2 Imports import Reflex.Dom
at the top
Step 3 Write a main function
mainWidget will take over main body of html page.
Once you have mainwidget you can write the html DOM element widgets. The most basic widget is text
. Text is the lowest level. It won’t put it in a p tag or div tag or anything else. It is just plain text that is shown in the browser.
-- workshop.hs
-- entry point for any haskell application is the main function
main :: IO ()
main = mainWidget text "hello world"
A page could be any level of complexity (login page, dashboard, etc.) but for now we start with compiling the basic text.
Step 4 In your Nix shell, type ghcjs workshop.hs
It creates a folder workshop.jsexe with a bunch of js files. Index.html just loads the javascript files that the compiler used. Open the html file in a browser and you’ll see what it produces. You can open up the chrome inspector to look at specific DOM tags produced.
What are the files produced by ghcjs?
- index.html is the main file to view in the browser
- manifest.webapp is for firefox offline apps (untested)
- all.js is the main file. That’s the only one you need for production.. the other js files are combined into that file.
What is the build system doing when you type ghcjs?
Ghcjs can also work with the regular cabal system using cabal --configure ghcjs
. When using reflex-platform, it’ll be handled with nix by default so you can make a cabal file and edit the workon script to customize for the cabal file instead of the default file provided.
Tip: make reflex-platform a submodule in git so that the version is locked down. (similar to docker but more declarative) so that things may break but you’re not forced to update builds.
API Client Tutorial
We’re going to use NASA’s api, available at https://api.nasa.gov/api.html#apod
Let’s design a login page with one text input for the API key.
Text Input with Reflex-Dom
textInput
returns a text input. it has a value, dynamic events (key press, focused, etc) but we’re only interested in the value right now. it’s a dynamic value. We read the docs and quickref to find out the type signatures.
_textInput_value
gives us back a Dynamic t String
type
dynText
takes a Dynamic String
and returns a dom element.
dynText :: Dynamic String -> m ()
Adding this to the top ofy our file {-# LANGUAGUE ScopedTypeVariables #-}
lets us write the type signatures in workshop.hs.
Now with the api key input, we want a button also. Clicking on the button fires an event.
button :: String -> m (Event())
We include the button along with the text input on our form.
workshop :: MonadWidget t m => m ()
workshop = do
text "hello world"
t <- textInput def --def provides default input
let apiKey = _textInput_value t
--doesn't have event yet, just a static button
b <- button "Send Request"
-- this says what happens after button fires, see below for notes
let apiKeyEvent = tagDyn apiKey b
--return unit because mainWidget requires the type return
return ()
Compile this file with ghcjs workshop.hs
again.
Now we have defined a dynamic string and an event. When the event fires we want to take the string input and make a request with that value and need the feature that supports that. Looking at reflex quickref again…
tagDyn :: Dynamic a -> Event b -> Event a
tagDyn says it takes value of dynamic and an event to create another event. In our example, it takes the button clicking event to trigger the next event that we’ll define next.
what is t? t represents the timeline, it comes from math theory.
Working with API Requests
Now we want to construct a request with the NASA api. Ali reminds us again:
- events tell you when value changes
- behaviors are lines that always have a value
We want to draw a line between dots, something will change value when the button is clicked. We’re going to use holdDyn
in this example. It takes an initial value (a) and takes an event.
holdDyn :: a -> Event a -> m (Dynamic a)
-- using forall tells compiler that all the t and m are the same
workshop' :: forall t m. MonadWidget t m => m ()
workshop' = do
text "hello world"
t <- textInput def
let apiKey = _textInput_value t
b :: Event t () <- button "Send Request"
let apiKeyEvent :: Event t String = tagDyn apiKey b
submittedApiKey :: Dynamic t String <- holdDyn "EMPTY" apiKeyEvent
dynText submittedApiKey
return ()
When do you use let vs <- binding when defining variables? You use let when defining values where as you bind monads. ConstDyn for example doesn’t have m so it’s not monadic.
When looking at quick ref, m
means you need an arrow thing. Another one is tagDyn doesn’t have monads (it’s a pure function) so you can use let. Results of a let doesn’t impact the reflex-dom program at all.
constDyn :: a -> Dynamic a
Tips:
- A monad is good for when you need a behavior.
- A pure function doesn’t have a side effect so it’s like text.
- At type level - bind arrow strips off monad. m() type: the bind gets rid of the m and gives you something.
Enter key pressed
Reacting to user interactions require dynamic events. Let’s look at reflex dom quick ref again to see the types.
textInputGetEnter :: TextInput -> Event ()
let apiKeyEnterEvent :: Event t String =
tagDyn apiKey (textInputGetEnter t)
We have two events that trigger something, but we only need one of them so we use a function that picks a default option from the pair. We use leftmost
to combine two events and returns one.
leftmost :: [Event a] -> Event a
Tip: monoid basically lets you combine instances. unit
()
is also a monoid
<>
` says if you have 2 events and the thing inside is monoidal then you can treat inside as monoidal. If they fire simultaneously and will concatenate everything side.
Constructing API requests
xhr lets you construct a request. performRequestAsync
makes the xhr request, transforming the event to another event using a function with the xhr request inside. fmap
is the gold old fashioned fmap from where ever you might have learned it… so combining the two looks like the following:
let req :: Event t XhrRequest = fmap apiKeyToXhrRequest apiKeyEvent
We want _xhrResponse_responseText from XhrResponse so let’s convert the type. We’ll have to import data.text since it doesn’t come with prelude.
import data.text
--(you can import qualified to avoid conflicts from prelude)
Tip: You can use ++ or <> to concatenate strings
We’re only interested in value if a response is returned, so we can make use of the Maybe type. fmap lets us apply functor to inside the event, event also has a maybe functor instance.
-- two events
workshop :: forall t m. MonadWidget t m => m ()
workshop = do
text "hello world"
t <- textInput def
let apiKey = _textInput_value t
b :: Event t () <- button "Send Request"
let apiKeyButtonEvent :: Event t String = tagDyn apiKey b
apiKeyEnterEvent :: Event t String = tagDyn apiKey (textInputGetEnter t)
-- if both events fire simultaneously, the 1st listed is picked
apiKeyEvent :: Event t String = leftmost [ apiKeyButtonEvent
, apiKeyEnterEvent
]
submittedApiKey :: Dynamic t String <- holdDyn "NO STRING SUBMITTED" apiKeyEvent
dynText submittedApiKey
--take string and turn it into an XhrRequest (metho, url, config)
let req :: Event t XhrRequest = fmap apiKeyToXhrRequest apiKeyEvent
-- bind response
rsp :: Event t XhrResponse <- performRequestAsync req
-- this is created after response, so it's an event type.
let rspText :: Event t (Maybe T.Text) = fmap _xhrResponse_responseText rsp
rspString :: Event t String = fmap (\rt -> T.unpack fromMaybe T.empty rt) rspText
-- expects a dynamic string but we have Event t (Maybe T.Text)
holdDyn "" rspString
return ()
-- function builds the request
apiKeyToXhrRequest :: String -> XhrRequest
apiKeyToXhrRequest k = XhrRequest { _xhrRequest_method = "GET"
, _xhrRequest_url = "https://api.nasa.gov/planetary/apod?api_key=" ++ k
, _xhrRequest_config = def
}
Parsing JSON
Finally, let’s use Aeson to parse json data. We only need FromJSON
type to decode. We can use decodeXhrResponse
from Reflex to decode the response.
show
will take value from something and turn it into some sort of string output
Tip If the aeson parser returns
Nothing
, it probably indicates a typo in the Nasapicture data type.
import Reflex.Dom
import qualified Data.Text as T
import Data.Maybe
import GHC.Generics
import Data.Aeson
import qualified Data.Map as Map
data NasaPicture = NasaPicture { copyright :: String
, date :: String
, explanation :: String
, hdurl :: String
, media_type :: String
, service_version :: String
, title :: String
, url :: String
}
deriving (Show, Generic)
instance FromJSON NasaPicture
workshop :: forall t m. MonadWidget t m => m ()
workshop = do
text "hello world"
t <- textInput def --def provides default input
let apiKey = _textInput_value t
b :: Event t () <- button "Send Request"
let apiKeyButtonEvent :: Event t String = tagDyn apiKey b
apiKeyEnterEvent :: Event t String = tagDyn apiKey (textInputGetEnter t)
apiKeyEvent :: Event t String = leftmost [ apiKeyButtonEvent
, apiKeyEnterEvent
]
submittedApiKey :: Dynamic t String <- holdDyn "NO STRING SUBMITTED" apiKeyEvent
dynText submittedApiKey
let req :: Event t XhrRequest = fmap apiKeyToXhrRequest apiKeyEvent
rsp :: Event t XhrResponse <- performRequestAsync req
let rspText :: Event t (Maybe T.Text) = fmap _xhrResponse_responseText rsp
rspString :: Event t String = fmapMaybe (\mt -> fmap T.unpack mt) rspText
respDyn <- holdDyn "No Response" rspString
-- have to tell it what the blog of text should represent
let decoded :: Event t (Maybe NasaPicture) = fmap decodeXhrResponse rsp
dynPic :: Dynamic t (Maybe NasaPicture) <- holdDyn Nothing decoded
dynPicString <- mapDyn show dynPic
-- if event you could fmap
-- but it's a dynamic so mapDyn takes function and map to an event
imgAttrs :: Dynamic t (Map.Map String String) <- forDyn dynPic \np ->
case np of
Nothing -> Map.empty
Just pic -> Map.singleton "src" (url pic)
-- create a map, takes a key & value with just that pair
-- reflex gives you syntactic sugar which can be
-- Just pic -> "src" =: url pic
elDynAttr "img" imgAttrs return ()
return ()
apiKeyToXhrRequest :: String -> XhrRequest
apiKeyToXhrRequest k = XhrRequest { _xhrRequest_method = "GET"
, _xhrRequest_url = "https://api.nasa.gov/planetary/apod?api_key=" ++ k
, _xhrRequest_config = def
}
Tips: map and formap are similar but different patterns, different order
import Data.Map
.. and sometimes you have to import bothData.Map (Map)
orData.Map as Map
Closing advice:
- read quick refs for Reflex and Reflex-Dom
- look at subreddit reflexfrp
- irc channel #reflex-frp
- ny-haskell slack