Getting Started with Reflex¶
In this first installment of the reflex tour, we’ll set up a stack-based infrastructure for compiling reflex programs, see some basic code, and see how we can compile and minify our app.
Contrary to the standard way of installing reflex, which is based on the
package manager, we’ll focus on a
stack based installation. The repo for
this tutorial is here.
Clone the entire repo, move to that folder and launch these installation steps:
stack build gtk2hs-buildtools
- Be sure to have the required system libraries (like
webkitgtk). If you miss some of the libraries, they will pop up as error in the next step, and you can install the missing ones
- Build with ghc:
- Execute the desktop app:
stack exec userValidation
- Build with ghcjs:
- Execute the web app:
- TODO: check that this works on macOS
Update: Instruction for macOS, on yosemite 10.10.5
git clone https://github.com/vacationlabs/haskell-webapps.git cd haskell-webapps/ cd UI/ReflexFRP/starterApp/ stack build gtk2hs-buildtools stack setup --stack-yaml=stack-ghcjs.yaml stack install happy stack build --stack-yaml=stack-ghcjs.yaml /Applications/Firefox.app/Contents/MacOS/firefox $(stack path --local-install-root --stack-yaml=stack-ghcjs.yaml)/bin/starterApp.jsexe/index.html
While all this builds (it will be a fairly lengthy process the first time), if you are a new reflex user, be sure to check the beginners tutorial (if you want an installation process based on stack for the same code, check out here.
You can see that there are two files: a stack.yaml
and a stack-ghcjs.yaml.
Both contain the same version of the libraries we’re using, but with this setup
we get a desktop app for free (built using webkit), and we’re able to use tools
for checking the code (like
ghc-mod) that don’t yet directly
Here below you can see the two versions of the app:
A look at the code¶
The first objective that this file has is to show how to deal with the fact that sometimes we don’t want our values to be updated continuously: for example when designing a form, we want the feedback from the program to happen only when something happens (like, the login button is clicked, or the user navigates away from the textbox
Let’s begin commenting the main function:
main :: IO () main = run 8081 $ mainWidgetWithHead htmlHead $ do el "h1" (text "A validation demo") rec firstName <- validateInput "First Name:" nameValidation signUpButton lastName <- validateInput "Last Name:" nameValidation signUpButton mail <- validateInput "Email:" emailValidation signUpButton age <- validateInput "Age:" ageValidation signUpButton signUpButton <- button "Sign up" let user = (liftM4 . liftM4) User firstName lastName mail age
The first function we’ll going to see is:
mainWidgetWithHead :: (forall x. Widget x ()) -> (forall x. Widget x ()) -> IO ()
This is the type of a
type Widget x = PostBuildT Spider (ImmediateDomBuilderT Spider (WithWebView x (PerformEventT Spider (SpiderHost Global))))
(it’s a bit scary, but I want to introduce it here because there is an error that happens sometimes when not constraing the monad enough, and this is the key to understand that. TODO, flesh out this section)
You don’t need to concern yourself with the exact meaning of this, it’s just a
convenient way to talk about a monadic transformer which hold the semantics
together. Usually we just pass to that function an argument of type
MonadWidget t m => m (), as you can see from:
htmlHead :: MonadWidget t m => m () htmlHead = do styleSheet "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" styleSheet "https://cdnjs.cloudflare.com/ajax/libs/milligram/1.1.0/milligram.min.css" where
In which we import the css files we need from a cdn.
As you can see, the structure of the main function denotates the components of this simple app, giving a name to the return values.
Note that the
RecursiveDo pragma lets us use the return value of the button
before of his definition. It’s useful to think at the main as having the
following meaning: in the first pass, the widgets are constructed, and
subsequently the reactive network continues the elaboration (TODO: I’m not sure
to include this visualization).
The most important functions are
notifyLogin, defined below:
validateInput :: MonadWidget t m => Prompt -- ^ The text on the label -> (Text -> Either Text a) -- ^ A pure validation function -> Event t b -- ^ An event so syncronize the update with
validateInput function is directly responsable for the rendering of the
label, using the pure function to validate the data, and change the value
reported back to the caller as soon as the button is pressed.
On the other hand, the function:
notifyLogin :: MonadWidget t m
is responsible for drawing the notification for the successful login as it happens.
With these suggestions in mind, you can read directly the source code which is thoroughly commented.
The ghcjs compiler by default generates some extra code dealing with
bindings: as we want only the webapp here, the first pass in the optimization is
-DGHCJS_BROWSER option to strip the node code from the generated
executable. We also use the new
-dedupe flags that optimizes for generated
size. All this is accomplished in this section of the cabal file:
if impl(ghcjs) ghc-options: -dedupe cpp-options: -DGHCJS_BROWSER else
The next step will be using google’s
closure compiler to minify the compiles
zopfli to gzip it; go ahead and install those
tools (I just did
sudo dnf install ccjs zopfli on fedora, but you can find
the relevant instructions on their github pages).
I included a simple deployment script to show how you could compile and minify your app (I’m purposefully creating a simple bash script, there are much more things you can do, check them at ghcjs deployment page).
#!/usr/bin/env bash # Compiling with ghcjs: stack build --stack-yaml=stack-ghcjs.yaml # Moving the generated files to the js folder: mkdir -p js cp -r $(stack path --local-install-root --stack-yaml=stack-ghcjs.yaml)/bin/starterApp.jsexe/all.js js/ # Minifying all.js file using the closure compiler: cd js ccjs all.js --compilation_level=ADVANCED_OPTIMIZATIONS > all.min.js # OPTIONAL: zipping, to see the actual transferred size of the app: zopfli all.min.js
Here’s the relevant output of
ls -alh js, to show the size of the generated files:
-rw-r--r--. 1 carlo carlo 3.0M Dec 12 17:16 all.js -rw-rw-r--. 1 carlo carlo 803K Dec 12 17:17 all.min.js -rw-rw-r--. 1 carlo carlo 204K Dec 12 17:17 all.min.js.gz
So, the final minified and zipped app is about 204 Kb, not bad since we have to bundle the entire ghc runtime (and that’s a cost that we only pay once, regardless of the size of our application).
We could also wonder if we have a size penalty from the fact that I used classy-prelude instead of manually importing all the required libraries. So I did an alternative benchmark, and it turns out that that’s not the case:
-rw-r--r--. 1 carlo carlo 3.1M Dec 12 17:35 all.js -rw-rw-r--. 1 carlo carlo 822K Dec 12 17:35 all.min.js -rw-rw-r--. 1 carlo carlo 206K Dec 12 17:35 all.min.js.gz
As you can see, the difference is really minimal. In fact, all the size is probably taken up by the encoding of the ghc runtime.