Adding Interactivity with Elm!
A couple weeks ago, we learned how we could serve HTML code from a Haskell server using Servant. Doing this without a system like Reflex left us with a quandry. We still want interactivity on our web pages. But we'd like to do this in a functional way, without writing a lot of Javascript.
Reflex FRP is only one of several options for writing frontend code in a functional language. A while back we considered how to do this with Elm. In this final part of our series, we'll combine Servant with Elm to produce an interactive page.
There are, of course, other options both for frontend and backend when making web apps. Take a look at our Production Checklist to learn more!
A Basic Counter
For a more in depth look at Elm, you should explore our full series on the topic. But for now, let's go over a quick and simple application that we could put in a browser. This app will have a "model" and we will pass "messages" using UI components.
The model will just be an integer counter. And we'll pass "increment" and "decrement" messages by clicking a couple of buttons. We start an Elm application by defining our model and message types. We'll use an alias for the integer model.
type alias Model = Int
type Msg = Increment | Decrement
Now we need to specify how each message type updates our model. An increment message will increase it, and a decrement message will decrease it.
update : Msg -> Model -> Model
update msg model =
case msg of
Increment -> model + 1
Decrement -> model - 1
Next we generate the HTML elements for our page with a view
function. This takes our Model
and returns Html
to display. We'll have two buttons to send the increment and decrement messages. Then we'll also display the current count.
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
view : Model -> Html Msg
view model =
div []
[ button [onClick Decrement] [text "-"]
, div [] [text (String.fromInt model) ]
, button [onClick Increment] [text "+"]
]
Finally, we generate our final application as a "main" expression. We use Browser.sandbox
, and pass an initial model value, as well as our update
and view
expressions.
import Browser
main = Browser.sandbox { init = 0, update = update, view = view}
Compiling Our Elm
Supposing we've written this code in an Elm project as Main.elm
, it's now quite easy to compile it into a full HTML file. We run this command:
elm make src/Main.elm
This produces a file index.html
that contains our full Elm application. The file requires a lot of boilerplate to get Elm working, so it's over 5000 lines long! But if we return that from our server instead of a blaze-generated HTML file, our app will work!
Referencing Elm
We could make manual modifications to this HTML file to do other things like adding our own CSS. But often times it's easier to compile the Elm into a Javascript file we can include with our other assets. To do this, we just have to tweak our command by outputting to a .js
file:
elm make src/Main.elm --output elm.js
This output file now contains all 5000 lines of our compiled Elm. Now we can make our own HTML file that looks like this:
<html>
<head>...</head>
<body>
<div id="elm"></div>
<script src="/static/elm.js"/>
<script>
var app = Elm.Main.init({node: document.getElementById("elm")});
</script>
</body>
</html>
The first script
includes our compiled app. The second, shorter script
does the work of embedding the app in the preceding div
. In this way, we can embed a smaller Elm application in along with other HTML components if we want to. It's much easier to swap out our other assets without having to re-compile our whole frontend!
Of course, for this to work, we have to use the techniques from our earlier article. Our Servant server must serve its static content from a particular directory. This will allow us to include elm.js
and any other scripts we make. And then it has to serve our HTML page for the desired endpoints.
Conclusion
You should now have enough tools at your disposal to make a simple web app using only functional tools! Our Real World Haskell Series will give you a good tutorial on some other parts of the stack. If you need any other tools, take a look at our Production Checklist. You can also take a look at all the code for this brief series on Github.
Starting next week, we're going to transition a bit. We've explored the idea of Haskell and machine learning before on this blog. Next time, we'll start looking at some ideas in game AI and agent development. This will set the stage for a combination of Open AI Gym and Haskell code.
Servant Testing Helpers!
We've been looking at Haskell and HTML code for a few weeks now. Last time, we introduced the idea of serving HTML content from a Servant server. Testing any server can be a little tricky. In our Real World Haskell Series, we explored some of the complications in testing such a server.
This week, we're going to look at a couple shortcuts we can take that will make testing our server a little easier. We'll examine some helpers from the Hspec.Wai library, as well as some Quickcheck expressions we can use with Servant.
For more useful Haskell tools, download our Haskell Production Checklist. It goes through a few other libraries you can use besides Servant for your web apps!
Simple Server
Let's start with the same basic user endpoint we had from last time. This takes a User ID, and returns a User
if the ID exists in our "database" (an in-memory map for our example).
data User = ...
instance ToJSON User where
...
type MyAPI = "users" :> Capture "uid" Int :> Get '[JSON] (Maybe User)
userHandler :: Int -> Handler (Maybe User)
userHandler uid = return $ M.lookup uid userDB
We've got a server to run this API, which we can use to make an Application
.
myServer :: Server MyAPI
myServer = userHandler
myApi :: Proxy MyAPI
myApi = Proxy
myApp :: Application
myApp = serve myApi myServer
runServer :: IO ()
runServer = run 8080 myApp
But, as with any IO action, testing the behavior of this server is tricky. In our earlier article, we had to jump through several hoops to run some tests using the Hspec library. As a pre-condition of the test, we had to start the IO
action. Then we had to construct requests using Servant's client functionality. Unwrapping and analyzing the responses was a bit annoying. Finally, we had to ensure we killed the server at the end. Luckily, there's a simpler way to go through this process.
Test Assertions
The HSpec Wai library attempts to simplify this process. As before, we write our tests in conjunction with Hspec syntax. This gives us the opportunity to put more descriptive information into our tests. Even better, we can use simple functions to send network requests to our server.
The expression we'll construct is a normal Spec
, so it can fit right in with any other tests we write using Hspec. The key to this process is the with
function, which takes our Application
as a parameter:
apiSpec :: Spec
apiSpec = with (return myApp) $ do
...
The next key is the get
combinator. This creates a test assertion we can use within one of our it
statements. It takes a string for the "route" of the request we are sending to our server. The simplest assertion we can make is to check the status code of the reply.
apiSpec :: Spec
apiSpec = with (return myApp) $ do
Describe "GET /users/1" $ do
it "responds with 200" $ do
get "users/1" `shouldRespondWith` 200
The assertion statement shouldRespondWith
takes a ResponseMatcher
type. We can make this matcher in a few different ways. Passing a simple number will make it only worry about the status code. If we pass a string, it will verify that the response body matches that string. Here's how we can verify that we receive a full user string when the user exists, and a "null" result when they don't.
apiSpec :: Spec
apiSpec = with (return myApp) $ do
describe "GET /users/1" $ do
it "responds with 200" $ do
get "/users/1" `shouldRespondWith` 200
it "responds with a user" $ do
get "/users/1" `shouldRespondWith`
"{'email':'james@test.com','age':25,'name':'James','id':1}"
describe "GET /users/5" $ do
it "responds with null" $ do
get "/users/5" `shouldRespondWith` "null"
Unfortunately there don't seem to be great mechanisms for verifying individual fields. You'll have to build any kind of custom matcher from scratch. It's certainly sub-optimal to have to match the JSON string exactly. We can reduce this burden a bit by using the JSON helper library. This let's us specify the object within a quasi-quoter so we don't have to be as precise in specifying the string:
{-# LANGUAGE QuasiQuotes #-}
apiSpec :: Spec
apiSpec = with (return myApp) $ do
describe "GET /users/1" $ do
...
it "responds with a user" $ do
get "/users/1" `shouldRespondWith`
[json|
{email: "james@test.com", age: 25, name: "James", id: 1}
|]
So there are definitely areas to improve this library. But it does provide some useful functionality.
Servant Quick Check
Another helpful way to test our API is to incorporate some of the techniques of Quickcheck. The servant-quickcheck library allows us to make blanket assertions about our API . It does this by sending many arbitrary requests to it.
We can actually incorporate these assertions into Hspec code as well. We start with a single assertion and withServantServer
:
quickcheckSpec :: Spec
quickcheckSpec =
it "API has good properties" $
withServantServer myApi (return myServer) ...
Our key function takes a proxy for our API as well as an action returning our server. The next part is a function taking a "URL" parameter. We'll then use the serverSatisfies
argument with our API and some defaultArgs
.
quickcheckSpec :: Spec
quickcheckSpec =
it "API has good properties" $
withServantServer myApi (return myServer) $ \burl ->
serverSatisfies myApi burl defaultArgs
...
The final piece is to build our actual assertions. We combine these with <%>
and need to use mempty
as a base. For a simple example, we can test that our endpoint never returns a 500 status code. We can also check that it never takes longer than a second (1e9 nanoseconds). Here's our complete assertion:
quickcheckSpec :: Spec
quickcheckSpec =
it "API has good properties" $
withServantServer myApi (return myServer) $ \burl ->
serverSatisfies myApi burl defaultArgs
(not500 <%> notLongerThan 1e9 <%> mempty)
Another assertion we can make is that our API only returns full JSON objects. The client code might depend on parsing these, rather than loose strings or some other format. In our case, this will actually fail with our API, because it can return null
.
quickcheckSpec :: Spec
quickcheckSpec =
it "API has good properties" $
withServantServer myApi (return myServer) $ \burl ->
serverSatisfies myApi burl defaultArgs
-- Check JSON as well!
(onlyJsonObjects <%>
not500 <%>
notLongerThan 1000000000 <%>
mempty)
This suggests we could reconsider how our API works. We could, for example, have it return a 404 instead of a null object if the user doesn't exist. These are some of the simplest functions in the You can take a look at the documentation for a complete listing.
Conclusion
Next week will be our last article on web applications for a little while. We'll explore what it will take to have "functional frontend" code. We'll use Purescript to generate Javascript and use this code within the HTML we send from our server!
Don't forget to subscribe to Monday Morning Haskell! This will give you access to our monthly newsletter as well as our subscriber resources!
Serving HTML with Servant
We now have several different ways of generating HTML code from Haskell. Our last look at this issue explored the Lucid library. But in most cases you won't be writing client side Haskell code.
You'll have to send the HTML you generate to your end-user, typically over a web server. So in this article we're going to explore the most basic way we can do that. We'll see how we can use the Servant library to send HTML in response to API requests.
For a more in-depth tutorial on making a web app with Servant, read our Real World Haskell series! You can also get some more ideas for Haskell libraries in our Production Checklist.
Servant Refresher
Suppose we have a basic User
type, along with JSON instances for it:
data User = User
{ userId :: Int
, userName :: String
, userEmail :: String
, userAge :: Int
}
instance FromJSON User where
…
instance ToJSON User where
...
In Servant, we can expose an endpoint to retrieve a user by their database ID. We would have this type in our API definition, and a handler function.
type MyAPI =
"users" :> Capture "uid" Int :> Get '[JSON] (Maybe User) :<|>
... -- other endpoints
userHandler :: Int -> Handler (Maybe User)
…
myServer :: Server MyAPI
myServer =
userHandler :<|>
... -- Other handlers
Our endpoint says that when we get a request to /users/:uid
, we'll return a User
object, encoded in JSON. The userHandler
performs the logic of retrieving this user from our database.
We would then let client side Javascript code actually to the job of rendering our user as HTML. But let's flip the script a bit and embrace the idea of "server side rendering." Here, we'll gather the user information and generate HTML on our server. Then we'll send the HTML back in reply. First, we'll need a couple pieces of boilerplate.
A New Content Type
In the endpoint above, the type list '[JSON]
refers to the content type of our output. Servant knows when we have JSON
in our list, it should include a header in the response indicating that it's in JSON.
We now want to make a content type for returning HTML. Servant doesn't have this by default. If we try to return a PlainText
HTML string, the browser won't render it! It will display the raw HTML string on a blank page!
So to make this work, we'll start with two types. The first will be HTML
. This will be our equivalent to JSON
, and it's a dummy type, with no actual data! The second will be RawHtml
, a simple wrapper for an HTML bytestring.
import Data.ByteString.Lazy as Lazy
data HTML = HTML
newtype RawHtml = RawHtml { unRaw :: Lazy.ByteString }
We'll use the HTML
type in our endpoints as we currently do with JSON
. It's a content type, and our responses need to know how to render it. This means making an instance of the Accept
class. Using some helpers, we'll make this instance use the content-type: text/html
header.
import Network.HTTP.Media ((//), (/:))
import Servant.API (Accept(..))
instance Accept HTML where
contenType _ = "text" // "html" /: ("charset", "utf-8")
Then, we'll link our RawHtml
type to this HTML
class with the MimeRender
class. We just unwrap the raw bytestring to send in the response.
instance MimeRender HTML RawHtml where
mimeRender _ = unRaw
This will let us use the combination of HTML
content type and RawHtml
result type in our endpoints, as we'll see. This is like making a ToJSON
instance for a different type to use with the JSON
content type.
An HTML Endpoint
Now we can rewrite our endpoint so that it returns HTML instead! First we'll make a function that renders our User
. We'll use Lucid in this case:
import Lucid
renderUser :: Maybe User -> Html ()
renderUser maybeUser = html_ $ do
head_ $ do
title_ "User Page"
link_ [rel_ "stylesheet", type_ "text/css", href_ "/styles.css"]
body_ $ userBody
where
userBody = case maybeUser of
Nothing -> div_ [class_ "login-message"] $ do
p_ "You aren't logged in!"
br_ []
a_ [href_ "/login"] "Please login"
Just u -> div_ [class_ "user-message"] $ do
p_ $ toHtml ("Name: " ++ userName u)
p_ $ toHtml ("Email: " ++ userEmail u)
p_ $ toHtml ("Age: " ++ show (userAge u))
Now we'll need to re-write our endpoint, so it uses our new type:
type MyAPI = "users" :> Capture "uid" Int :> Get '[HTML] RawHtml :<|>
...
Finally, we would rewrite our handler function to render the user immediately!
userHandler :: Int -> Handler RawHtml
userHandler uid = do
maybeUser <- fetchUser uid -- DB lookup or something
return (RawHtml $ renderHtml (renderUser maybeUser))
Our server would now work, returning the HTML string, which the browser would render!
Serving Static Files
There's one more thing we need to handle! Remember that HTML by itself is not typically enough. Our HTML files almost always reference other files, like CSS, Javascript, and images. When the user loads the HTML we send, they'll make another immediate request for those files. As is, our server won't render any styles for our user HTML. How do we serve these?
In Servant, the answer is the serveDirectoryWebApp
function. This allows us to serve out the files from a particular file as static files. The first piece of this puzzle is to add an extra endpoint to our server definition. This will catch all patterns and return a Raw
result. This means the contents of a particular file.
type MyAPI =
"users" :> Capture "uid" Int :> Get '[HTML] RawHtml :<|>
Raw
This endpoint must come last out of all our endpoints, even if we compose MyAPI
with other API types. Otherwise it will catch every request and prevent other handlers from operating! This is like when you use a catch-all too early in a case
statement.
Now for our "handler", we'll use the special serve
function.
myServer :: Server MyAPI
myServer =
userHandler <|>:
serveDirectoryWebApp "static"
And now, if we have styles.css
with appropriate styles, they'll render correctly!
Conclusion
It's a useful exercise to go through the process of making our HTML
content type manually. But Blaze and Lucid both have their own helper libraries to simplify this. Take a look at servant-blaze and servant-lucid. You can import the corresponding modules and this will handle the boilerplate for you.
Next week, we'll explore a few extra things we can do with Servant. We'll see some neat combinators that allow us to test our Servant API with ease!
Don't forget you can take a look at our Github repository for more details! This week's code is in `src/BasicServant.hs.
And also remember to subscribe to Monday Morning Haskell! You'll get our monthly newsletter and access to our subscriber resources!
Lucid: Another HTML Option
We're currently looking at different Haskell libraries for generating HTML code. We've already explored how to do this a bit in Reflex FRP and using the Blaze library. This week, we'll consider one more library, Lucid. Then next week we'll start looking at some more complex things we can do with our generated code.
The approaches from Reflex and Blaze have a lot of similarities. In particular, both use monadic composition for building the tree. Lucid will continue this theme as well, and it will generally have a lot in common with Blaze. But there are a few differences as well, and we'll explore those a bit.
If you want to play around with the code from this article a bit more, you should clone our Github repository! This repo contains the simpler Blaze and Html code, as well as some ways we'll use it. If you're ready to work on a full web application, you can also read our Real World Haskell series. This will walk you through the basics of building a web backend with a particular library stack. You can also download our Production Checklist to learn about more options!
Similar Basics
Hopefully, you've already gotten familiar with Blaze's syntax. But even if you're not, we're going to dive straight into Lucid. This syntax is pretty straightforward, as long as you know the basic HTML markup symbols. Here's the input form example we did last time, only now, using Lucid:
{-# LANGUAGE OverloadedStrings #-}
module LucidLib where
import Lucid
mainHtml :: Html ()
mainHtml = html_ $ do
head_ $ do
title_ "Random Stuff"
link_ [rel_ "stylesheet", type_ "text/css", href_ "screen.css"]
body_ $ do
h1_ "Welcome to our site!"
h2_ $ span_ "New user?"
div_ [class_ "create-user-form"] $ do
form_ [action_ "createUser"] $ do
input_ [type_ "text", name_ "username"]
input_ [type_ "email", name_ "email"]
input_ [type_ "password", name_ "password"]
input_ [type_ "submit", name_ "submit"]
br_ []
h2_ $ span_ "Returning user?"
div_ [class_ "login-user-form"] $ do
form_ [action_ "login"] $ do
input_ [type_ "email", name_ "email"]
input_ [type_ "password", name_ "password"]
input_ [type_ "submit", name_ "submit"]
br_ []
Right away things look pretty similar. We use a monad to compose our HTML tree. Each new action we add in the monad adds a new item in the tree. Our combinators match the names of HTML elements.
But there are, of course, a few differences. For example, we see lists for attributes instead of using the !
operator. Every combinator and attribute name has underscores. Each of these differences has a reason, as outlined by the author Chris Done in his blog post. Feel free to read this for some more details. Let's go over some of these differences.
Naming Consistency
Let's first consider the underscores in each element name. What's the reason behind this? In a word, the answer is consistency. Let's recall what blaze looks like:
import Text.Blaze.Html5 as H
import Text.Blaze.Html5.Attributes as A
blazeHtml :: Html
blazeHtml = docTypeHtml $ do
H.head $ do
H.title "Our web page"
body $ do
h1 "Welcome to our site!"
H.div ! class_ "form" $ do
p "Hello"
Notice first the qualified imports. Some of the elements conflict with Prelude
functions. For example, we use head
with normal lists and div
for mathematics. Another one, class_
, conflicts with a Haskell keyword, so it needs an underscore. Further, we can use certain combinators, like style
, either as a combinator or as an attribute. This is why we have two imports at the top of our page. It allows us to use H.style
as a combinator or A.style
as an attribute.
Just by adding an underscore to every combinator, Lucid simplifies this. We only need one import, Lucid
, and we have consistency. Nothing needs qualifying.
Attribute Lists
Another difference is attributes. In Blaze, we used the !
operator to compose attributes. So if we want several attributes on an item, we can keep adding them like so:
-- Blaze
stylesheet :: Html
stylesheet =
link ! rel "stylesheet" ! href "styles.css" ! type_ "text/css"
Lucid's approach rejects operators. Instead we use a list to describe our different attributes. Here's our style element in Lucid:
-- Lucid
stylesheet :: Html ()
stylesheet =
link_ [rel_ "stylesheet", type_ "text/css", href_ "screen.css"]
In a lot of ways this syntax is cleaner. It's easier to have lists as extra expressions we can reuse. It's much easier to append a new attribute to a list than to compose a new expression with operators. At least, you're much more likely to get the type signature correct. Ultimately this is a matter of taste.
One reason for Blaze's approach is to avoid empty parameters on a large number of combinators. If a combinator can take a list as a parameter, what do you do if there are no attributes? You either have []
expressions everywhere or you make a whole secondary set of functions.
Lucid gets around this with some clever test machinery. The following two expressions have the same type, even though the first one has no attributes!
aDiv :: Html ()
aDiv = div_ $ p "Hello"
aDiv2 :: Html ()
aDiv2 = div_ [class_ "hello-div"] $ p_ "Hello"
Due to the class Term
, we can both have a normal Html
element follow our div
, or we can list some attributes first. Certain empty combinators like br_
don't fit this pattern as well. They can't have sub-elements, so we need the extra []
parameter, as you can see above. This pattern is also what enables us to use the same style
combinator in both situations.
Rendering
There are other details as well. The Monad instance for Html
is better defined in Lucid. Lucid's expressions also have a built-in Show
instance, which makes simple debugging better.
For Blaze's part, I'll note one advantage comes in the rendering functionality. It has a "pretty print" renderer, that makes the HTML human readable. I wasn't able to find a function to do this from poking around with Lucid. You can render in Lucid like so:
import Lucid
main :: IO ()
main = renderToFile "hello.html" mainHtml
mainHtml :: Html ()
mainHtml = ...
You'll get the proper HTML, but it won't look very appetizing.
Conclusion
So at the end of the day, Blaze and Lucid are more similar than they are different. So the choice is more one of taste. Now, we never want to produce HTML in isolation. We almost always want to serve it out to users of a more complete system. Next week, we'll start looking at some options for using the Servant library to send HTML to our end users.
There are many different pieces to building a web application! For instance, you'll need a server backend and a database! Download our Production Checklist to learn some more libraries you can use for those!
Blaze: Lightweight Html Generation
We've now got a little experience dealing with Haskell and HTML. In our last article we saw how to use some basic combinators within Reflex FRP to generate HTML. But let's take a step back and consider this problem in a simpler light. What if we aren't doing a full Reflex app? What if we just want to generate an HTML string in the context of a totally different application? Suppose we're using some other library to run our backend and want to send some HTML as a raw string. How can we generate this string?
We wouldn't go through the full effort of setting up a Nix application to run GHCJS and Reflex. We would like to do this with a simple Stack application. In the next couple weeks, we'll consider two simple libraries we can use to generate HTML code. This week, we'll look at the Blaze HTML library. Next week we'll consider Lucid. Then after that, we'll investigate how we can serve the HTML we generate from a Servant server.
For some more ideas of production-ready libraries, download our Production Checklist! Try out some other platforms for database management or frontend development!
Basic Combinators
Let's start with the basics. Blaze has a few things in common with the Reflex method of generating HTML data. It also uses a monadic type to produce the HTML tree. In Blaze, this monad is just called Html
. Each new action produces a new element node in the tree. Most every basic HTML element has its own function in the library. So we can start our tree with the basic html
tag, and then provide a head
element as well as a body
.
{-# LANGUAGE OverloadedStrings #-}
import Text.Blaze.Html5 as H
Import Text.Blaze.Html5.Attributes as A
basicHtml :: Html
basicHtml = html $ do
H.head $ do
H.title "My HTML page"
body $ do
h1 "Welcome to our site!"
In some cases, the HTML element names conflict with Haskell library functions. So we use a qualified import with the letter H
or A
to be more specific.
The above example will produce the following HTML:
<html>
<head>
<title>My HTML Page</title>
</head>
<body>
<h1>Welcome to our site!"</h1>
</body>
</html>
We can get this as a string by using renderHtml
from one of a few different modules in the library. For instance the "Pretty" renderer will give the above format, which is more human readable:
import Text.Blaze.Html.Renderer.Pretty
producePage :: String
producePage = renderHtml basicHtml
We can take our simple HTML now and add a few more elements. For instance, we can also add a "doctype" tag at the top, specifying that it is, in fact HTML. This saves us from needing the basic html
combinator. We can also do nesting of different elements, such as lists:
basicHtml :: Html
basicHtml = docTypeHtml $ do
H.head $ do
H.title "My HTML page"
body $ do
h1 "Welcome to our site!"
"This is just raw text"
ul $ do
li "First item"
li "Second item"
li "Third item"
One final observation here is that we can use raw strings as a monadic element. We need the OverloadedStrings
extension for this to work. This just makes a raw text item in the HTML tree, without any wrapper. See how the raw text appears in our output here:
<!DOCTYPE HTML>
<html>
<head>
<title>My HTML Page</title>
</head>
<body>
<h1>Welcome to our site!"</h1>
This is just raw text
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
</ul>
</body>
</html>
Attributes
Now a key component of HTML is, of course, to use attributes with different items. This allows us to customize them with styles and various other properties. For example, when we use an image element, we should provide a "source" file as well as alternate text. We add different attributes to our items with the !
operator. This operator composes so we can add more attributes. Here is an example:
logoImage :: Html
logoImage = img ! src "logo.png" ! alt "The website's logo"
-- HTML
<img src="logo.png" alt="The website's logo"/>
Naturally, we'll want to use CSS with our page. In the head
element we can add a stylesheet using a link
element. Then we can apply classes to individual components using class_
.
styledHtml :: Html
styledHtml = docTypeHtml $ do
H.head $ do
link ! rel "stylesheet" ! href "styles.css"
body $ do
div ! class_ "style-1" $ do
"One kind of div"
div ! class_ "style-2" $ do
"A second kind of div"
Using Haskell to Populate Types
Now since our Html
elements are normal Haskell expressions, we can use any kind of Haskell type as an input. This can turn our elements into functions that depend on normal application data. For example, we can make a list out of different names:
renderNames :: [String] -> Html
renderNames names = do
"Here are the names"
ul $ forM_ names (li . toHtml)
We can also take a more complex data structure and use it as an input to our HTML elements. In this example, we'll show a user their points total if we have a User
object. But if not, we'll encourage them to login instead.
data User = User
{ userName :: String
, userPoints :: Int
}
pointsDisplay :: Maybe User -> Html
pointsDisplay Nothing = a ! href "/login" $ "Please login!"
pointsDisplay (Just (User name points)) = div ! class_ "user-points" $ do
"Hi "
toHtml name
"!"
br
"You have "
toHtml points
" points!"
This sort of idea is at the heart of "server side rendering", which we'll explore later on in this series.
Making a Form
Here's one final example, where we'll provide two different forms. One for creating a user account, and one for logging in. They each link to separate actions:
multiformPage :: Html
multiformPage = do
H.head $ do
H.title "Our Page"
link ! rel "stylesheet" ! href "styles.css"
body $ do
h1 "Welcome to our site!"
h2 $ H.span "New user?"
H.div ! class_ "create-user-form" $ do
H.form ! action "createUser" $ do
input ! type_ "text" ! name "username"
input ! type_ "email" ! name "email"
input ! type_ "password" ! name "password"
input ! type_ "submit" ! name "submit"
br
h2 $ H.span "Returning user?"
H.div ! class_ "login-user-form" $ do
H.form ! action "login" $ do
input ! type_ "email" ! name "email"
input ! type_ "password" ! name "password"
input ! type_ "submit" ! name "submit"
As we can see, monadic syntax gives us a very natural way to work with this kind of "tree building" operation.
Conclusion
Now while we've reduced our dependencies from Reflex, this library does have limitations. There's no clear form of Haskell based dynamism. To make our page dynamic, we'd have to include Javascript files along with our generated HTML! And most of us Haskell developers don't want to be writing much Javascript if we can avoid it.
There are still other ways we can use functional means to get the Javascript we want, besides Reflex! We'll explore those a bit later on.
So Blaze has some limitations, but it serves its purpose well. It's a lightweight way of generating HTML in a very intuitive way. Next week, we'll explore another library, Lucid, that has a similar goal.
You can also take a look at our Github repository to see the full code example for this article!
Download our Production Checklist to learn more! If you liked this article, you might want to consider reading our series on Purescript and Elm!
Reflex HTML Basics
Last week we used Nix to create a very simple application using the Reflex FRP framework. This framework uses the paradigm of Functional Reactive Programming to create web pages. It allows us to use functional programming techniques in a problem space with a lot of input and output.
In this week's article, we're going to start explore this framework some more. We'll start getting a feel for the syntax Reflex uses for making different HTML elements. Once we're familiar with these basics, we can compare Reflex with other frontend Haskell tools.
There are several different options you can explore for making these kinds of pages. For some more ideas, download our Production Checklist. This will also suggest some different libraries you can use for your web app's backend!
A Main Function
Let's start out by looking at the code for the very basic page we made last week. It combines a few of the simplest functions we'll need to be familiar with in Reflex.
{-# LANGUAGE OverloadedStrings #-}
module Frontend.Index where
runIndex :: main ()
runIndex = mainWidget $ el "div" $ text "Welcome to Reflex!"
There are three different functions here: mainWidget
, el
, and text
. The mainWidget
function is our interface between Reflex types and the IO
monad. It functions a bit like a runStateT
function, allowing us to turn our page into a normal program we can run. Here is its type signature:
mainWidget :: (forall t. Widget t ()) -> IO ()
We provide an input in some kind of a Widget
monad and it will convert it to an IO action. The t
parameter is one we'll use throughout our type signatures. Reflex FRP will implicitly track a lot of different events on our page over time. This parameter signifies a particular "timeline" of events.
We won't need to get into too much detail about the parameter. There's only one case where different expressions can have different t
parameters. This would be if we have multiple Reflex apps at the same time, and we won't get into this case.
There are other main
functions we can use. Most likely, we would want to use mainWidgetWithCss
for an full project. This takes a CSS string to apply over our page. We'll want to use the embedFile
template function here. This converts a provided filepath into the actual CSS ByteString
.
mainWidgetWithCss :: ByteString -> (forall t. Widget t()) -> IO ()
runIndex = do
let cssString = $(embedFile "static/styles.css")
mainWidgetWithCss cssString $ el "div" $ text "Hello, Reflex!"
Static Elements
The rest of our combinators will have HTML oriented types. We'll start with our two simple combinators, text
and el
. These are both different kinds of "widgets" we can use.
The first of these is straightforward enough. It takes a string (Text
) and produces an element in a DomBuilder
monad. The result of this will be a simple text element appearing on our webpage with nothing wrapping it.
text :: (DomBuilder t m) => Text -> m ()
So for example if we omitted the use of el
above, the HTML for our web page body would look like:
<body>
Welcome to Reflex!
</body>
The el
combinator then provides us with the chance to wrap one HTML element within another. We provide a first argument with a string for the type of element we're wrapping with. Then we give the monadic action for the HTML element within. In the case of our page, we wrap our original text element with a div
.
el :: (DomBuilder t m) => Text -> m () -> m ()
runIndex = mainWidget $ el "div" $ text "Welcome to Reflex!"
This produces the following HTML in our body:
<body>
<div>Welcome to Reflex!</div>
</body>
Now, because an element takes a monad, we can compose more elements within it as deeply as we want. Here's an example with a couple nested lists:
runIndex = mainWidget $ el "div" $ do
el "p" (text "Two Lists")
el "ol" $ do
el "li" (text "Number One")
el "li" (text "Number Two")
el "li" (text "Number Three")
el "ul" $ do
el "li" (text "First Item")
el "li" (text "Second Item")
el "li" (text "Third Item")
Adding Attributes
Of course, there's more to HTML than creating elements. We'll also want to assign properties to our elements to customize their appearance.
One simple way to do this is to use the elAttr
combinator instead of el
. This allows us to provide a map of attributes and values. Here's an example where we provide the filename, width, and height of an image element. Note that blank
is the same as text ""
, an empty HTML element:
imageElement = elAttr "image"
("src" =. "checkmark.jpg" <> "height" =. "300" <> "width" =. "300")
blank
-- Produced HTML
<img src="checkmark.jpg" height="300" width="300"></img>
Reflex has some specific combinators we can use to build an attribute map. The =.
operator combines two elements to create a singleton map. We can append different maps with the monoid operator <>
.
In general, we should handle CSS with static files elsewhere. We would create CSS classes that contain many different properties. We can then apply these classes to our HTML elements. The elClass
combinator is an easy way to do thing in Reflex.
styledText = elClass "p" "fancy" (text "Hello")
-- Produced HTML
<p class="fancy">Hello</p>
Now we don't need to worry about styling every individual element.
Conclusion
We already have quite a few opportunities available to us to build our page. Still, it was a big hassle to use Nix and Reflex just to write some Html. Next week, we'll start exploring more lightweight options for doing this in Haskell.
For more resources on building Haskell web tools, download our Production Checklist!
Making the Jump to Real World Haskell
Last week, we announced our Practical Haskell course. Enrollments are still open, but not for much longer! They will close at midnight Pacific time on Wednesday, March 11th, only a couple days from now!
I've always hoped to provide content that would help people make the jump from beginners to seasoned Haskell developers. I want to show that Haskell can be useful for "Real World" applications. Those are the main goals of this course. So in this article, I wanted to share some of the mistakes I made when I was trying to make that jump. These are what motivated me to make this course, so I hope you can learn from them.
Package Management is Key
My Haskell career started with a side project, one you can still see on Github. There were some cool things about the project, but my process had several flaws. The first one was that I had no idea how to organize a Haskell project.
My early work involved writing all my code in .hs
source files and running manual tests with runghc
. Installing dependencies was a mess (I put everything in the global package database). I eventually learned to use Cabal, but without sandboxing. Dependency hell ensued. It was only after months of working through that process that I learned about Stack. Stack made everything easier, but I could have used it from the start!
Don't repeat my mistake! Learn how to use Stack, or just Cabal, or even Nix! This will solve so many of your early problems. It will also streamline the rest of your development process. Speaking of...
Test First, Integrate Completely
When it comes to making a project, the first question you should ask is, "How will my customer use this?" When it comes to writing code within that project, you should always then ask, "How will I know this code works?"
These two questions will guide your development and help avoid unnecessary rework. It's a natural tendency of developers that we want to jump in on the "meat" of the problem. It's exactly the mistake I made on that first project. I just wanted to write Haskell! I didn't want to worry about scripting or package non-sense. But these issues will ultimately get in the way of what you really want to do. So it's worth putting in the effort to overcome them.
The first step of the project as a whole should be to build out your end-to-end pipeline. That is, how will you put this code out there on the web? How will someone end up using your code? There will often be tedious scripting involved, and dealing with services (CI, AWS, etc.). But once that work is out of the way, you can make real progress.
Then when developing a particular component, always know how you'll test it. Most often, this will be through unit testing. But sometimes you'll find it's more complicated than that. Nothing's more frustrating than thinking you're done coding and finding problems later. So it's important to take the time to learn about the frameworks that let you test things with ease. Keep practicing over and over again until testing is second nature.
Start Simple
Another important thing when it comes to the learning process is knowing how to start small. I learned this over the course of my machine learning series last fall. My methods were often so ineffective that I didn't know if the algorithm I was trying to implement worked at all. But the problem I was trying to solve was too difficult! I've found more success in machine learning by starting with simpler problems. This way, you'll know the general approach works, and you can scale up accordingly.
This also makes it much easier to follow the advice above! If your system is large and complicated, the scripting and running process will be harder. You'll have to spend more time getting everything up and running. For a smaller project, this is not so difficult. So you'll get valuable practice at a smaller scale. This will make bigger projects smoother once you get there.
Use Both Documentation and Examples
None of us were born knowing how to write Haskell. The first time you use a library, you won't know the best practices. The documentation can help you. It'll list everything you need, but often a lot more. It can be hard to know what's necessary and what's not.
So another great thing to do when starting out is to find a project that has used the library before. You need to establish some baseline of "something that works". This way, you'll have a more solid foundation to build on. You'll have specific examples to work from, which will help build your "end-to-end experience".
In my first project, I used the Parsec library without using any examples! My code was sloppy and repetitive. There were many shortcuts I didn't know about hiding in the docs. And I could have avoided that if I had first looked for a project that also used the library. Then I could have started from there and built my knowledge.
Documentation and examples work in tandem with each other. If you use the docs without examples, you'll miss a lot of shortcuts and practical uses. If you use examples without the docs, you'll miss the broader picture of what else you can do! So both are necessary to your development as a programmer.
Conclusion
Haskell has developed a lot in the last few years, so this is a great time to learn what the language is capable of! Our Practical Haskell course aims to help you become a more seasoned developer. It'll help you avoid all the mistakes I outlined in this article. So if you've got the basics down and want to learn more, this is your chance!
And if you're not as confident in your skills yet, you can also check out our Beginners course! It requires no experience and will walk you through the basics!
Announcing Practical Haskell!
This week we have an exciting announcement! One of the biggest goals of this blog has been to show Haskell's utility as a production language. Our Practical Haskell course is the culmination of that goal. It assumes you have a decent grounding in Haskell basics, including things like monads and using Stack. To sign up, head over to the course page!
Course Overview
The course consists of five modules. Each module has a series of video lectures and accompanying exercises. There are also "screencast" videos where you get to see the techniques from the lectures in action.
Throughout the course, you'll be building a small web application. In the first module, you'll learn how to store the necessary data for this app. We'll take an in-depth look at Persistent, a Haskell database library. This will show us some of the unique features Haskell can bring to this area.
In module 2, we'll learn how to build a web server that provides an API for accessing our database. We'll see how to write endpoints using the Servant library and how we can test those endpoints. We'll also deploy our application using Heroku.
Module 3 provides a frontend for our app. This frontend will be in Elm, rather than Haskell! These languages have very similar syntax. So you'll learn some special libraries and techniques for integrating them. This way, you'll be able to display all the data you're serving!
The fourth module will teach you some advanced ideas for organizing your code. We'll take a deep dive into monad transformers and also learn about free monads!
We'll wrap up the course with an overview of testing in Haskell. We'll start with some common unit testing libraries and work our way up to more advanced techniques.
Besides the course material, there will also be a Slack group for this course. This will be a place where you can get help from myself or any of your fellow classmates!
Course Schedule
The course will launch on Monday, March 16th, with the release of module 1. We will then release a new module each Monday thereafter. Don't worry if you're busy on a particular week! There's no time limit on doing the material. You'll be able to access the content indefinitely.
Sign-ups for the course will end on Wednesday, March 11th! So don't miss out! Head over to the course page and reserve your spot today!
Building a Reflex FRP Project with Nix!
Over these last few weeks, we've gone over a few different package managers we can use with Haskell. Of these, Nix has the broadest potential, but it's also the most complicated to use. This week, we'll conclude our look at package management by putting Nix to work. We'll be using Reflex FRP, a Haskell framework for frontend web development.
This framework has many complicated dependencies, so it's basically necessary to use Nix. Stack and Cabal by themselves aren't going to be able to capture all the requirements. In this article, we'll go through some steps to set ourselves up to make a Reflex FRP project. Along the way, we'll learn a little bit about the basics of this library.
If you're ready for Nix and FRP frontend apps, you're ready to use Haskell for many cool things! Download our Production Checklist for some more ideas!
Project Setup
Before you do this setup phase, take note that it will take quite a while due to the volume of packages. We'll be following along with this guide. You'll want to make a new directory with access to the reflex-platform
Git repository.
>> mkdir ReflexProject && cd ReflexProject
>> git init
>> git submodule add https://github.com/reflex-frp/reflex-platform
You then need to run the try-reflex
script within that repository. If you don't have Nix, this will install Nix. But regardless, it will then use Nix to install many dependencies needed for Reflex. So feel free to run the command and let your terminal sit for a while.
Managing Multiple Packages
For this project, we'll be using a combination of Nix and Cabal, but in a different way than we tried a couple weeks ago. Back then, we converted a .cabal
file to a Nix project. For Reflex FRP, the recommended project structure is to use three different packages. First we would want one for frontend web elements. The second is for a backend server, while we'd have a third common
package for elements used by both. To reflect this structure you'll make three different directories. Then run cabal init
in each of them.
>> mkdir common frontend backend
>> (cd common && cabal init)
>> (cd frontend && cabal init)
>> (cd backend && cabal init)
The common
package will be a library and you should ensure it exposes at least one module. The other two packages should be executables that depend on common
. All three should depend on reflex-dom
.
Now we need to make a default.nix
file to pull this all together. It should look like this:
{ system ? builtins.currentSystem }:
(import ./reflex-platform { inherit system; }).project ({ pkgs, ... }: {
packages = {
common = ./common;
backend = ./backend;
frontend = ./frontend;
};
shells = {
ghc = ["common" "backend" "frontend"];
ghcjs = ["common" "frontend"];
};
})
The simplest part to see is that we list each of our different packages within this expression. Then we declare two "shells" for the different compilers we can use. We can compile any of our 3 packages with GHC. But then for our frontend
and its common
dependency, we also have the option of using GHCJS. We'll see this come into play in a little bit.
The important thing to notice at the top is that we are using the reflex-platform
submodule as a dependency. This lets us avoid worrying about a lot of other dependencies in this file.
A Simple Frontend
Now let's take a step back from our project structure for a second and write out a basic frontend landing page. This code will go in frontend/Main.hs
:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Reflex.Dom
main :: IO ()
main = mainWidget $ el "div" $ text "Welcome to Reflex!"
The Reflex.Dom
module exports most of the different elements we'll need to build a simple web page. The mainWidget
expression provides us with a way to run a widget as an IO
action. Then el
allows us to make an element of the specified type (a div
in this case). We can then provide some simple text using the text
element. As you can probably tell, this will give us a webpage that displays our desired text.
We'll definitely explore more details about Reflex FRP syntax at a later date. But for now, the important thing to understand is that we have a Haskell executable that displays a webpage.
Building Our Code with Nix
But how do actually use this executable? Well there are a couple ways. The first way uses Nix by itself:
>> nix-build -o frontend-result -A ghcjs.frontend
Note how we're using the ghcjs
shell to compile this instead of ghc
. GHCJS knows how to generate Javascript from Haskell, rather than a raw binary.
We can then look in the output directory, frontend-result/bin
to see the results. There's another directory frontend.jsexe
in there. It contains several Javascript helper files, but one main index.html
file. If we pull this index file into our browser, we'll see our web page text!
Building with Cabal
Relying on Nix for building does have a weakness though. Nix doesn't do incremental builds. So it will need to build our whole package every time. This can be frustrating if you like re-building many times after small changes.
So we can also build our frontend with Cabal, which knows how to do things in a more incremental way! We'll still use a nix-shell
to ensure we have our all our dependencies.
>> nix-shell -A shells.ghcjs --run \
"cabal --project-file=cabal-ghcjs.project \
--builddir=dist-ghcjs new-build all"
Note how we use the GHCJS project to again ensure we get the right output. This will also build our code, producing the same frontend.jsexe
directory. This time though, it will live within dist-ghcjs
.
A small price of using Cabal is that we have to dig deeper in the file structure to find it (eight directories)! But scripting can relieve this burden. The point is, we can now incrementally build our frontend executable!
Conclusion
This concludes our series on package management! You should now have the tools to get started with some pretty interesting projects. You can use any of the different programs we've covered here, whether Cabal, Stack, Nix, or a combination!
This marks a nice transition point for us. From here we'll move on and look at some interesting concepts when it comes to web development. We'll stick with the general app structure we started building here with Reflex FRP. It will be a little while before we look at frontend concepts in earnest though. We'll start by exploring more backend considerations next week.
The coming weeks' topics will be more related to Haskell in production! Download our Production Checklist for more ideas to try!
Building a Better Brain
In the last few weeks, we've focused a lot on the player AI for our game. We've used a few more advanced tricks to help our player navigate the maze using drills. But that's come at a performance cost. The game can now get a little choppy when there are a lot of enemies, or when our player is far away from the goal. It also takes longer to run our analysis iterations than we would like.
This week, we'll improve the performance of our AI by caching the determined path. Lots of our calculations for shortest path measurements get repeated. We can keep track of these, and avoid the entire BFS algorithm altogether in a lot of circumstances!
This week, you should take a look at the search-caching
branch on our Github repository for the complete code we're implementing here. We'll focus on changes in the MazeUtils.hs
file.
We're also going to do a little bit of profiling for this article. Profiling your code is an important skill to learn about if you ever want to use Haskell in production. For some other useful skills, check out our Production Checklist!
Profiling Our Code
As alluded to above, we have a pretty good idea of where the performance bottleneck is for our code. But it always pays to be sure. So to double check, we're going to run our code under profiling. We'll go through some of the basics here, but you should also check out this article we did on profiling a while back.
We'll get a readout for our code that will tell us which functions are taking the most time. This will tell us where we can make the most effective improvements. It will also give us a concrete way to prove our improvement later.
To start, we'll need to rebuild our code with stack build --profile
. Be warned this can take a while, since all the libraries also need to be re-built. Then we can re-run the analysis program we used last week:
stack exec -- analyze-game maze_save_2 --enemies +RTS -p
Here's the abbreviated readout in the file `analyze-game.EXE.prof:
total time = 32.62 secs
COST CENTRE %time
drillBFS.newParentsMap.\ 21.9
drillBFS.unvisitedNextItems.\ 21.7
drillBFS.newVisitedSet 19.4
getDrillAdjacentItems 6.2
drillBFS 4.5
drillBFS.newSearchQueue 4.0
getDrillAdjacentItems.mkItemFromResult 3.0
bfs.newParentsMap.\ 2.1
bfs.newVisitedSet 2.0
getDrillAdjacentItems.mkItemFromResult.(...) 1.7
drillBFS.unvisitedNextItems 1.4
bfs.unvisitedNextCells.\ 1.1
drillBFS.newParentsMap 1.0
getDrillAdjacentItems.bounds 1.0
bfs 0.6
getAdjacentLocations 0.5
Unsurprisingly, we see that drillBFS
and it's helpers are the biggest culprits. They account for the top seven entries on the list and a whopping 82% of the time we spend. The enemy AI calculations come in a distant second at around 6.3% of the time. So let's focus on fixing the player algorithm.
A Basic Cache for the Player
As we try to improve our player AI, there's one big observation we can make. Perhaps some of you already noted this when reading about that AI in the first place. For the most part, our player follows a single path the whole time. We calculate the complete path from start to finish on each player move cycle, but then throw most of it away. The only time we get "blown off" this path is when we have to run away from enemies.
There are only a few circumstances where we change this path! So let's make PlayerMemory
type that will keep track of it. This should save us a ton of time!
newtype PlayerMemory = PlayerMemory (Maybe [Location])
data Player = Player
{ …
, playerMemory :: PlayerMemory
}
We'll add this memory to our player type. When we initialize it from JSON instances, it should start out empty. There's no need to keep track of this in a save-game file.
This change will complicate our move API a little bit. It will now produce the PlayerMemory
as an output:
makePlayerMove :: World -> (PlayerMove, PlayerMemory)
Using Our Memory
When it comes to making out move, we first need to put the path into memory. To start, we'll make PlayerMemory
out of the path we get from BFS.
makePlayerMove :: World -> (PlayerMove, PlayerMemory)
makePlayerMove w =
( PlayerMove finalMoveDirection useStun drillDirection
, ...
)
where
shortestPath = getShortestPathWithDrills …
memoryFromMove = PlayerMemory (Just shortestPath)
...
In general, we'll want to return this "memory". But there's one circumstance where we'll want to invalidate it. When we have to retreat from our enemies, we'll diverge from this ideal path. In this case, we'll return Nothing
. Here's what that logic looks like:
makePlayerMove :: World -> (PlayerMove, PlayerMemory)
makePlayerMove w =
( PlayerMove finalMoveDirection useStun drillDirection
, if emptyCache then (PlayerMemory Nothing) else memoryFromMove
)
where
(finalMoveDirection, useStun, emptyCache) = if not enemyClose
then (shortestPathMoveDirection, False, False)
else if canStun
then (shortestPathMoveDirection, True, False)
else case find (/= shortestPathMoveLocation) possibleMoves of
Nothing -> (DirectionNone, False, True)
Just l -> (getMoveDirection playerLoc, False, True)
Now let's consider when we use the cached information, as this will let us skip the BFS call altogether! We'll add one more validity check when doing this. We'll ensure that the list is non-empty and that our current location is at the head of the list. Then we can use the tail of the memory list as the shortest path call!
makePlayerMove :: World -> (PlayerMove, PlayerMemory)
makePlayerMove w = ...
where
(useCache, cachePath) = case playerMemory currentPlayer of
(PlayerMemory (Just (first : rest))) ->
(first == playerLoc, rest)
_ -> (False, [])
shortestPath = if useCache then cachePath
else getShortestPathWithDrills ...
The last thing we need is to ensure that the cache goes back into memory. This is a simple modification of our function for making the player move:
modifyWorldForPlayerMove :: World -> Location -> PlayerMemory -> World
modifyWorldForPlayerMove w newLoc memory = ...
where
currentPlayer = worldPlayer w
playerWithMemory = currentPlayer {playerMemory = memory}
playerAfterMove = movePlayer newLoc playerWithMemory
...
Now we can run our analysis again. We'll see that our Player's AI functions are still the biggest contributor. But the percentage has gone down a lot. They now take only take up around 55% of our total time, instead of 82%! Meanwhile, the percentage of time from the normal BFS functions is now up to around 35%. Most importantly, the total time for the analysis declined five-fold. On the first run, it was 32.62 seconds, and it now only takes 6.79 seconds, a huge improvement!
total time = 6.79 secs
COST CENTRE %time
drillBFS.unvisitedNextItems.\ 14.3
drillBFS.newParentsMap.\ 14.2
drillBFS.newVisitedSet 12.6
bfs.newParentsMap.\ 9.9
bfs.newVisitedSet 9.2
bfs.unvisitedNextCells.\ 5.7
getDrillAdjacentItems 4.3
drillBFS.newSearchQueue 2.8
getAdjacentLocations 2.8
drillBFS 2.6
bfs 2.6
getDrillAdjacentItems.mkItemFromResult 2.0
bfs.newSearchQueue 1.8
getDrillAdjacentItems.mkItemFromResult.(...) 1.1
bfs.unwindPath 1.1
bfs.unvisitedNextCells 1.0
drillBFS.unvisitedNextItems 0.9
bfs.newParentsMap 0.7
Conclusion
Profiling is an important tool we can use for improving our code, no matter what language we're working in. When our program isn't performing how we like, we have to be sure to address the right parts of it. It may have been tempting to make a different assumption from the start. Since there are many enemy characters, it would be natural to tackle that algorithm first. But our profiling output made it clear that the player AI was the problem.
Next week, we'll start exploring different AI concepts. We'll start moving towards a kind of AI that can be machine-learned. Our code will be simpler, but our product won't be as good, at least at the start! But we'll start getting used to the way an AI can evaluate positions.
For more useful resources in improving your Haskell skills, download our Production Checklist! It has a lot of different tools and libraries to check out!
Serializing Mazes!
Last week we improved our game so that we could solve additional random mazes after the first. This week, we'll step away from the randomness and look at how we can serialize our mazes. This will allow us to have a consistent and repeatable game. It will also enable us to save the game state later.
We'll be using the Megaparsec library as part of this article. If you aren't familiar with that (or parsing in Haskell more generally), check out our Parsing Series!
A Serialized Representation
The serialized representation of our maze doesn't need to be human readable. We aren't trying to create an ASCII art style representation. That said, it would be nice if it bore some semblance to the actual layout. There are a couple properties we'll aim for.
First, it would be good to have one character represent one cell in our maze. This dramatically simplifies any logic we'll use for serializing back and forth. Second, we should layout the cell characters in a way that matches the maze's appearance. So for instance, the top left cell should be the first character in the first row of our string. Then, each row should appear on a separate line. This will make it easier to avoid silly errors when coming up with test cases.
So how can we serialize a single cell? We could observe that for each cell, we have sixteen possible states. There are 4 sides, and each side is either a wall or it is open. This suggests a hexadecimal representation.
Let's think of the four directions as being 4 bits, where if there is a wall, the bit is set to 1, and if it is open, the bit is set to 0. We'll order the bits as up-right-down-left, as we have in a couple other areas of our code. So we have the following example configurations:
- An open cell with no walls around it is
0
. - A totally surrounded cell is
1111 = F
. - A cell with walls on its top and bottom would be
1010 = A
. - A cell with walls on its left and right would be
0101 = 5
.
With that in mind, we can create a small 5x5 test maze with the following representation:
98CDF
1041C
34775
90AA4
32EB6
And this ought to look like so:
This serialization pattern lends itself to a couple helper functions we'll use later. The first, charToBoundsSet
, will take a character and give us four booleans. These represent the presence of a wall in each direction. First, we convert the character to the hex integer. Then we use patterns about hex numbers and where the bits lie. For instance, the first bit is only set if the number is at least 8. The last bit is only set for odd numbers. This gives us the following:
charToBoundsSet :: Char -> (Bool, Bool, Bool, Bool)
charToBoundsSet c =
( num > 7,
, num `mod` 8 > 3
, num `mod` 4 > 1
, num `mod` 2 > 0
)
Then, we also want to go backwards. We want to take a CellBoundaries
item and convert it to the proper character. We'll look at each direction. If it's an AdjacentCell
, it contributes nothing to the final Int
value. But otherwise, it contributes the hex digit value for its place. We add these up and convert to a char with intToDigit
:
cellToChar :: CellBoundaries -> Char
cellToChar bounds =
let top = case upBoundary bounds of
(AdjacentCell _) -> 0
_ -> 8
let right = case rightBoundary bounds of
(AdjacentCell _) -> 0
_ -> 4
let down = case downBoundary bounds of
(AdjacentCell _) -> 0
_ -> 2
let left = case leftBoundary bounds of
(AdjacentCell _) -> 0
_ -> 1
in toUpper $ intToDigit (top + right + down + bottom)
We'll use both of these functions in the next couple parts.
Serializing a Maze
Let's move on now to determining how we can take a maze and represent it as Text
. For this part, let's first apply a type synonym on our maze type:
type Maze = Map.Map Location CellBoundaries
dumpMaze :: Maze -> Text
dumpMaze = ...
First, let's imagine we have a single row worth of locations. We can convert that row to a string easily using our helper function from above:
dumpMaze = …
where
rowToString :: [(Location, CellBoundaries)] -> String
rowToString = map (cellToChar . snd)
Now we'd like to take our maze map and group it into the different rows. The groupBy
function seems appropriate. It groups elements of a list based on some predicate. We'd like to take a predicate that checks if the rows of two elements match. Then we'll apply that against the toList
representation of our map:
rowsMatch :: (Location, CellBoundaries) -> (Location, CellBoundaries) -> Bool
rowsMatch ((_, y1), _) ((_, y2), _) = y1 == y2
We have a problem though because groupBy
only works when the elements are next to each other in the list. The Map.toList
function will give us a column-major ordering. We can fix this by first creating a transposed version of our map:
dumpMaze maze = …
where
transposedMap :: Maze
transposedMap = Map.mapKeys (\(x, y) -> (y, x)) maze
Now we can go ahead and group our cells by row:
dumpMaze maze = …
where
transposedMap = …
cellsByRow :: [[(Location, CellBoundaries)]]
cellsByRow = groupBy (\((r1, _), _) ((r2, _), _) -> r1 == r2)
(Map.toList transposedMap)
And now we can complete our serialization function! We get the string for each row, and combine them with unlines
and then pack
into a Text
.
dumpMaze maze = pack $ (unlines . reverse) (rowToString <$> cellsByRow)
where
transposedMap = …
cellsByRow = …
rowToString = ...
As a last trick, note we reverse
the order of the rows. This way, we get that the top row appears first, rather than the row corresponding to y = 0
.
Parsing a Maze
Now that we can dump our maze into a string, we also want to be able to go backwards. We should be able to take a properly formatted string and turn it into our Maze
type. We'll do this using the Megaparsec
library, as we discussed in part 4 of our series on parsing in Haskell. So we'll create a function in the Parsec
monad that will take the dimensions of the maze as an input:
import qualified Text.Megaparsec as M
mazeParser :: (Int, Int) -> M.Parsec Void Text Maze
mazeParser (numRows, numColumns) = ...
We want to parse the input into a format that will match each character up with its location in the (x,y)
coordinate space of the grid. This means parsing one row at a time, and passing in a counter argument. To make the counter match with the desired row, we'll use a descending list comprehension like so:
mazeParser (numRows, numColumns = do
rows <- forM [(numRows - 1), (numRows - 2)..0] $ \i -> do
...
For each row, we'll parse the individual characters using M.hexDigit
and match them up with a column index:
mazeParser (numRows, numColumns = do
rows <- forM [0..(numRows - 1)] $ \i -> do
(columns :: [(Int, Char)]) <-
forM [0..(numColumns - 1)] $ \j -> do
c <- M.hexDigitChar
return (j, c)
...
We conclude the parsing of a row by reading the newline character. Then we make the indices match the coordinates in discrete (x,y) space. Remember, the "column" should be the first item in our location.
mazeParser (numRows, numColumns = do
(rows :: [[(Location, Char)]]) <-
forM [0..(numRows - 1)] $ \i -> do
columns <- forM [0..(numColumns - 1)] $ \j -> do
c <- M.hexDigitChar
return (j, c)
M.newline
return $ map (\(col, char) -> ((col, i), char)) columns
...
Now we'll need a function to convert one of these Location, Char
pairs into CellBoundaries
. For the most part, we just want to apply our charToBoundsSet
function and get the boolean values. Remember these tell us if walls are present or not:
mazeParser (numRows, numColumns = do
rows <- …
where
cellSpecToBounds :: (Location, Char) -> (Location, CellBoundaries)
cellSpecToBounds (loc@(x, y), c) =
let (topIsWall, rightIsWall, bottomIsWall, leftIsWall) =
charToBoundsSet c
...
Now it's a matter of applying a case by case basis in each direction. We just need a little logic to determine, in the True
case, if it should be a Wall
or a WorldBoundary
. Here's the implementation:
cellSpecToBounds :: (Location, Char) -> (Location, CellBoundaries)
cellSpecToBounds (loc@(x, y), c) =
let (topIsWall, rightIsWall, bottomIsWall, leftIsWall) =
charToBoundsSet c
topCell = if topIsWall
then if y + 1 == numRows
then WorldBoundary
else Wall
else (AdjacentCell (x, y + 1))
rightCell = if rightIsWall
then if x + 1 == numColumns
then WorldBoundary
else Wall
else (AdjacentCell (x + 1, y))
bottomCell = if bottomIsWall
then if y == 0
then WorldBoundary
else Wall
else (AdjacentCell (x, y - 1))
leftCell = if leftIsWall
then if x == 0
then WorldBoundary
else Wall
else (AdjacentCell (x - 1, y))
in (loc, CellBoundaries topCell rightCell bottomCell leftCell)
And now we can complete our parsing function by applying this helper over all our rows!
mazeParser (numRows, numColumns = do
(rows :: [[(Location, Char)]]) <-
forM [0..(numRows - 1)] $ \i -> do
columns <- forM [0..(numColumns - 1)] $ \j -> do
c <- M.hexDigitChar
return (j, c)
M.newline
return $ map (\(col, char) -> ((col, i), char)) columns
return $ Map.fromList (cellSpecToBounds <$> (concat rows))
where
cellSpecToBounds = ...
Conclusion
This wraps up our latest part on serializing maze definitions. The next couple parts will still be more code-focused. We'll look at ways to improve our data structures and an alternate way of generating random mazes. But after those, we'll get back to adding some new game features, such as wandering enemies and combat!
To learn more about serialization, you should read our series on parsing. You can also download our Production Checklist for more ideas!
Generating More Difficult Mazes!
In the last part of this series, we established the fundamental structures for our maze game. But our "maze" was still rather bland. It didn't have any interior walls, so getting to the goal point was trivial. In this next part, we'll look at an algorithm for random maze generation. This will let us create some more interesting challenges. In upcoming parts of this series, we'll explore several more related topics. We'll see how to serialize our maze definition. We'll refactor some of our data structures. And we'll also take a look at another random generation algorithm.
If you've never programmed in Haskell before, you should download our Beginners Checklist! It will help you learn the basics of the language so that the concepts in this series will make more sense. The State
monad will also see a bit of action in this part. So if you're not comfortable with monads yet, you should read our series on them!
Getting Started
We represent a maze with the type Map.Map Location CellBoundaries
. For a refresher, a Location
is an Int
tuple. And the CellBoundaries
type determines what borders a particular cell in each direction:
type Location = (Int, Int)
data BoundaryType = Wall | WorldBoundary | AdjacentCell Location
data CellBoundaries = CellBoundaries
{ upBoundary :: BoundaryType
, rightBoundary :: BoundaryType
, downBoundary :: BoundaryType
, leftBoundary :: BoundaryType
}
An important note is that a Location
refers to the position in discrete x,y
space. That is, the first index is the column (starting from 0) and the second index is the row. Don't confuse row-major and column-major ordering! (I did this when implementing this solution the first time).
To generate our maze, we'll want two inputs. The first will be a random number generator. This will help randomize our algorithm so we can keep generating new, fresh mazes. The second will be the desired size of our grid.
import System.Random (StdGen, randomR)
…
generateRandomMaze
:: StdGen
-> (Int, Int)
-> Map.Map Location CellBoundaries
generateRandomMaze gen (numRows, numColumns) = ...
A Simple Randomization Algorithm
This week, we're going to use a relatively simple algorithm for generating our maze. We'll start by assuming everything is a wall, and we've selected some starting position. We'll use the following depth-first-search pattern:
- Consider all cells around us
- If there are any we haven't visited yet, choose one of them as the next cell.
- "Break down" the wall between these cells, and put that new cell onto the top of our search stack, marking it as visited.
- If we have visited all other cells around us, pop this current location from the stack
- As long as there is another cell on the stack, choose it as the current location and continue searching from step 1.
There are several pieces of state we have to maintain throughout the process. So the State
monad is an excellent candidate for this problem! Let's make a SearchState
type for all these:
data SearchState = SearchState
{ randomGenerator :: StdGen
, locationStack :: [Location]
, currentBoundaries :: Map.Map Location CellBoundaries
, visitedCells :: Set.Set Location
}
dfsSearch :: State SearchState ()
dfsSearch = ...
Each time we make a random selection, we'll use the randomR
function that returns the appropriate value as well as a new generator. Then we'll use a normal list for our search stack since we can push and pop from the top with ease. Next, we'll track the current state of the maze (it starts as all walls and we'll gradually break those down). Finally, there's the set of all cells we've already visited.
Starting Our Search!
To start our search process, we'll pull all our information out of the state monad, and examine the stack. If it's empty, we're done and can return! Otherwise, we'll want to consider the top location:
dfsSearch = do
(SearchState gen locs bounds visited) <- get
case locs of
[] -> return ()
(currentLoc : rest) -> do
...
Finding New Search Candidates
Given a particular location, we need to find the potential neighbors. We want to satisfy two conditions:
- It shouldn't be in our
visited
set. - The boundary to this location should be a
Wall
Then we'll want to use these properties to determine a list of candidates. Each candidate will contain 4 items:
- The next location
- The bounds we would use for the new location
- The previous location
- The new bounds for the previous location.
This seems like a lot, but it'll make more sense as we fill out our algorithm. With that in mind, here's the structure of our findCandidates
function:
findCandidates
:: Location -- Current location
-> Map.Map Location CellBoundaries -- Current maze state
-> Set.Set Location -- Visited Cells
-> [(Location, CellBoundaries, Location, CellBoundaries)]
findCandidates currentLocation bounds visited = ...
Filling in this function consists of following the same process for each of the four directions from our starting point. First we check if the adjacent cell in that direction is valid. Then we create the candidate, containing the locations and their new boundaries. Since the location could be invalid, the result is a Maybe
. Here's what we do for the "up" direction:
findCandidates =
let currentLocBounds = fromJust $
Map.lookup currentLocation bounds
upLoc = (x, y + 1)
maybeUpCandidate = case
(upBoundary currentLocBounds, Set.member upLoc visited) of
(Wall, False) -> Just
( upLoc
, (fromJust $ Map.lookup upLoc bounds)
{ downBoundary = AdjacentCell currentLocation }
, currentLocation
, currentLocBounds { upBoundary = AdjacentCell upLoc }
)
...
We replace the existing Wall
elements with AdjacentCell
elements in our maze map. This may seem like it's doing a lot of unnecessary work in calculating bounds that we'll never use. But remember that Haskell is lazy! Any candidate that isn't chosen by our random algorithm won't be fully evaluated. We repeat this process for each direction and then use catMaybes
on them all:
findCandidates =
let currentLocBounds = fromJust $ Map.lookup currentLocation bounds
upLoc = (x, y + 1)
maybeUpCandidate = …
rightLoc = (x + 1, y)
maybeRightCandidate = …
downLoc = (x, y - 1)
maybeDownCandidate = …
leftLoc = (x - 1, y)
maybeLeftCandidate = …
in catMaybes [maybeUpCandidate, maybeRightCandidate, … ]
Choosing A Candidate
Our search function is starting to come together now. Here's what we've got so far. If we don't have any candidates, we'll reset our search state by popping the current location off our stack. Then we can continue the search by making another call to dfsSearch
.
dfsSearch = do
(SearchState gen locs bounds visited) <- get
case locs of
[] -> return ()
(currentLoc : rest) -> do
let candidateLocs = findCandidates currentLoc bounds visited
if null candidateLocs
then put (SearchState gen rest bounds visited) >> dfsSearch
else ...
But assuming we have a non-empty list of candidates, we'll need to choose one. This function will update most of our state elements, so we'll put in in the State
monad as well:
chooseCandidate
:: [(Location, CellBoundaries, Location, CellBoundaries)]
-> State SearchState ()
chooseCandidate candidates = do
(SearchState gen currentLocs boundsMap visited) <- get
...
First, we'll need to select a random index into this list, which assumes it is non-empty.:
chooseCandidate candidates = do
(SearchState gen currentLocs boundsMap visited) <- get
let (randomIndex, newGen) = randomR (0, (length candidates) - 1) gen
(chosenLocation, newChosenBounds, prevLocation, newPrevBounds) =
candidates !! randomIndex
Since we did the hard work of creating the new bounds objects up above, the rest is straightforward. We'll create our new state with different components.
We get a new random generator from the randomR
call. Then we can push the new location onto our search stack. Next, we update the bounds map with the new locations. Last, we can add the new location to our visited array:
chooseCandidate candidates = do
(SearchState gen currentLocs boundsMap visited) <- get
let (randomIndex, newGen) = randomR (0, (length candidates) - 1) gen
(chosenLocation, newChosenBounds, prevLocation, newPrevBounds) =
candidates !! randomIndex
newBounds = Map.insert prevLocation newPrevBounds
(Map.insert chosenLocation newChosenBounds boundsMap)
newVisited = Set.insert chosenLocation visited
newSearchStack = chosenLocation : currentLocs
put (SearchState newGen newSearchStack newBounds newVisited)
Then to wrap up our DFS, we'll call this function at the very end. Remember to make the recursive call to dfsSearch
!
dfsSearch = do
(SearchState gen locs bounds visited) <- get
case locs of
[] -> return ()
(currentLoc : rest) -> do
let candidateLocs = findCandidates currentLoc bounds visited
if null candidateLocs
then put (SearchState gen rest bounds visited) >> dfsSearch
else (chooseCandidate candidateLocs) >> dfsSearch
Incorporating Our Search
As a last step in our process, we need to incorporate our search function. At the most basic level, we'll want to execute our DFS state function and extract the boundaries from it:
generateRandomMaze :: StdGen -> (Int, Int) -> Map.Map Location CellBoundaries
generateRandomMaze gen (numRows, numColumns) =
currentBoundaries (execState dfsSearch initialState)
where
initialState :: SearchState
initialState = ...
But we need to build our initial state. We'll start our search from a random location. Our initial stack and visited set will contain this location. Notice that with each random call, we use a new generator.
generateRandomMaze gen (numRows, numColumns) =
currentBoundaries (execState dfsSearch initialState)
where
(startX, g1) = randomR (0, numColumns - 1) gen
(startY, g2) = randomR (0, numRows - 1) g1
initialState :: SearchState
initialState = SearchState
g2
[(startX, startY)]
… -- TODO Bounds
(Set.fromList [(startX, startY)])
The last thing we need is our initial bounds set. For this, I'm going to tease the next part of the series. We'll write a function to parse a maze from a string representation (and reverse the process). Our encoding will represent a "surrounded" cell with the character 'F'. So we can represent a completely blocked maze like so:
generateRandomMaze gen (numRows, numCols) = …
where
…
fullString :: Text
fullString = pack . unlines $
take numRows $ repeat (take numColumns (repeat 'F'))
Finally, we'll apply the mazeParser
function in Megaparsec style. You'll have to wait a couple weeks to see how to implement that! It will give us the appropriate cell boundaries.
generateRandomMaze gen (numRows, numColumns) =
currentBoundaries (execState dfsSearch initialState)
where
(startX, g1) = randomR (0, numColumns - 1) gen
(startY, g2) = randomR (0, numRows - 1) g1
initialState :: SearchState
initialState = SearchState
g2
[(startX, startY)]
initialBounds
(Set.fromList [(startX, startY)])
initialBounds :: Map.Map Location CellBoundaries
initialBounds = case Megaparsec.runParser
(mazeParser (numRows, numColumns) "" fullString of
Right bounds -> bounds
_ -> error "Couldn't parse maze for some reason!"
fullString :: Text
fullString = ...
You can also look at our Github repo for some details. You'll want the part-2
branch if you want more details about how everything works!
Conclusion
Generating random mazes is cool. But it would be nice if we could actually finish the maze we're running and do another one! Next week, we'll make some modifications to the game state so that when we finish with one maze, we'll have the option to try another random one!
If you're just getting started with Haskell, we have some great resources to get you going! Download our Beginners Checklist and read our Liftoff Series!
Building a Bigger World
Last week we looked at some of the basic components of the Gloss library. We made simple animations and simulations, as well as a very simple "game" taking player input. This week, we're going to start making a more complex game!
Our game will involve navigating a maze, from start to finish. In fact, this week, we're not even going to make it very "mazy". We're just going to set up an open grid to navigate around with our player. But over the course of these next few weeks, we'll add more and more features, like enemies and hazards. At some point, we'll have so many features that we'll need a more organized scheme to keep track of everything. At that point, we'll discuss game architecture. You can take a look at the code for this game on our Github repository. For this part, you'll want to look at the part-1
branch.
Game programming is only one of the many interesting ways we can use Haskell. Take a look at our Production Checklist for some more ideas!
Making Our World
As we explored in the last part, the World
type is central to how we define our game. It is a parameter to all the important functions we'll write. Before we define our World
though, let's define a couple helper types. These will clarify many of our other functions.
-- Defined in Graphics.Gloss
-- Refers to (x, y) within the drawable coordinate system
type Point = (Float, Float)
-- Refers to discrete (x, y) within our game grid.
type Location = (Int, Int)
data GameResult = InProgress | PlayerWin | PlayerLoss
Let's start our World
type now with a few simple elements. We'll imagine the game board as a grid with a fixed size, with the tiles having coordinates like (0,0)
in the bottom left. We'll want a start location and an ending location for the maze. We'll also want to track the player's current location as well as the current "result" of the game:
data World = World
{ playerLocation :: Location
, startLocation :: Location
, endLocation :: Location
, gameResult :: GameResult
…
}
Now we need to represent the "maze". In other words, we want to be able to track where the "walls" are in our grid. We'll make a data type to represent to boundaries for any particular cell. Then we'll stick a mapping from each location in our grid to its boundaries:
data BoundaryType = WorldBoundary | Wall | AdjacentCell Location
data CellBoundaries = CellBoundaries
{ upBoundary :: BoundaryType
, rightBoundary :: BoundaryType
, downBoundary :: BoundaryType
, leftBoundary :: BoundaryType
}
data World = World
{ …
, worldBoundaries :: Map Location CellBoundaries
}
Populating Our World
Next week we'll look into how we can generate interesting mazes. But for now, our grid will only have "walls" on the outside, not in the middle. To start, we'll define a function that takes the number of rows and columns in our grid and a particular location. It will return the "boundaries" of the cell at that location. Each boundary tells us if there is a wall in one direction, or if we are clear to move to a different cell. All we need to check is if we're about to exceed the boundary in that direction.
simpleBoundaries :: (Int, Int) -> Location -> CellBoundaries
simpleBoundaries (numColumns, numRows) (x, y) = CellBoundaries
(if y + 1 < numRows
then AdjacentCell (x, y+1)
else WorldBoundary)
(if x + 1 < numColumns
then AdjacentCell (x+1, y)
else WorldBoundary)
(if y > 0 then AdjacentCell (x, y-1) else WorldBoundary)
(if x > 0 then AdjacentCell (x-1, y) else WorldBoundary)
Our main function now will loop through all the different cells in our grid and make a map out of them:
boundariesMap :: (Int, Int) -> Map.Map Location CellBoundaries
boundariesMap (numColumns, numRows) = Map.fromList
(buildBounds <$> (range ((0,0), (numColumns, numRows))))
where
buildBounds :: Location -> (Location, CellBoundaries)
buildBounds loc =
(loc, simpleBoundaries (numColumns, numRows) loc)
Now we have all the tools we need to populate our initial world:
main = play
windowDisplay
white
20
(World (0, 0) (0,0) (24, 24) InProgress (boundariesMap (25, 25))
drawingFunc ...
inputHandler …
updateFunc ...
Drawing Our World
Now we need to draw our world. We'll begin by passing a couple new parameters to our drawing function. We'll need offset values that will tell us the Point
in our geometric coordinate system for the Location
(0,0). We'll also take a floating point value for the cell size. Then we will also, of course, take the World
as a parameter:
drawingFunc :: (Float, Float) -> Float -> World -> Picture
drawingFunc (xOffset, yOffset) cellSize world = …
Before we do anything else, let's define a type called CellCoordinates
. This will contain the Points
for the center and four corners of a cell in our grid.
data CellCoordinates = CellCoordinates
{ cellCenter :: Point
, cellTopLeft :: Point
, cellTopRight :: Point
, cellBottomLeft :: Point
, cellBottomRight :: Point
}
Next, let's define a conversion function from a Location
to one of the coordinate objects. This will take the offsets, cell size, and the desired location.
locationToCoords ::
(Float, Float) -> Float -> Location -> CellCoordinates
locationToCoords (xOffset, yOffset) cellSize (x, y) = CellCoordinates
(centerX, centerY) -- Center
(centerX - halfCell, centerY + halfCell) -- Top Left
(centerX + halfCell, centerY + halfCell) -- Top Right
(centerX - halfCell, centerY - halfCell) -- Bottom Left
(centerX + halfCell, centerY - halfCell) -- Bottom Right
where
(centerX, centerY) =
( xOffset + (fromIntegral x) * cellSize
, yOffset + (fromIntegral y) * cellSize)
halfCell = cellSize / 2.0
Now we can go ahead and make the first few simple pictures in our game. We'll have colored polygons for the start and end locations, and a circle for the player token. The player marker is easiest:
drawingFunc (xOffset, yOffset) cellSize world =
Pictures [startPic, endPic, playerMarker]
where
conversion = locationToCoords (xOffset, yOffset) cellSize
(px, py) = cellCenter (conversion (playerLocation world))
playerMarker = translate px py (Circle 10)
startPic = …
endPic = ...
We find its coordinates through our conversion, and then translate a circle. For our start and end points, we'll want to do something similar, except we want the corners, not the center. We'll use the corners as the points in our polygons and draw these polygons in appropriate colors.
drawingFunc (xOffset, yOffset) cellSize world =
Pictures [startPic, endPic, playerMarker]
where
conversion = locationToCoords (xOffset, yOffset) cellSize
...
startCoords = conversion (startLocation world)
endCoords = conversion (endLocation world)
startPic = Color blue (Polygon
[ cellTopLeft startCoords
, cellTopRight startCoords
, cellBottomRight startCoords
, cellBottomLeft startCoords
])
endPic = Color green (Polygon
[ cellTopLeft endCoords
, cellTopRight endCoords
, cellBottomRight endCoords
, cellBottomLeft endCoords
])
Now we need to draw the wall lines. So we'll have to loop through the wall grid, drawing the relevant lines for each individual cell.
drawingFunc (xOffset, yOffset) cellSize world = Pictures
[mapGrid, startPic, endPic, playerMarker]
where
…
mapGrid = Pictures $concatMap makeWallPictures
(Map.toList (worldBoundaries world))
makeWallPictures :: (Location, CellBoundaries) -> [Picture]
makeWallPictures ((x, y), CellBoundaries up right down left) = ...
When drawing the lines for an individual cell, we'll use thin lines when there is no wall. We can make these with the Line
constructor and the two corner points. But we want a separate color and thickness to distinguish an impassable wall. In this second case, we'll want two extra points that are offset so we can draw a polygon. Here's a helper function we can use:
drawingFunc (xOffset, yOffset) cellSize world = ...
where
...
drawEdge :: (Point, Point, Point, Point) ->
BoundaryType -> Picture
drawEdge (p1, p2, _, _) (AdjacentCell _) = Line [p1, p2]
drawEdge (p1, p2, p3, p4) _ =
Color blue (Polygon [p1, p2, p3, p4])
Now to apply this function, we'll need to do a little math to dig out all the individual coordinates out of this cell.
drawingFunc (xOffset, yOffset) cellSize world =
Pictures [mapGrid, startPic, endPic, playerMarker]
where
...
makeWallPictures :: (Location, CellBoundaries) -> [Picture]
makeWallPictures ((x,y), CellBoundaries up right down left) =
let coords = conversion (x,y)
tl@(tlx, tly) = cellTopLeft coords
tr@(trx, try) = cellTopRight coords
bl@(blx, bly) = cellBottomLeft coords
br@(brx, bry) = cellBottomRight coords
in [ drawEdge (tr, tl, (tlx, tly - 2), (trx, try - 2)) up
, drawEdge (br, tr, (trx-2, try), (brx-2, bry)) right
, drawEdge (bl, br, (brx, bry+2), (blx, bly+2)) down
, drawEdge (tl, bl, (blx+2, bly), (tlx+2, tly)) left
]
But that's all we need! Now our drawing function is complete!
Player Input
The last thing we need is our input function. This is going to look a lot like it did last week. We'll only be looking at the arrow keys. And we'll be updating the player's coordinates if the move they entered is valid. To start, let's figure out how we get the bounds for the player's current cell (we'll assume the location is in our map).
inputHandler :: Event -> World -> World
inputHandler event w = case event of
(EventKey (SpecialKey KeyUp) Down _ _) -> ...
(EventKey (SpecialKey KeyDown) Down _ _) -> ...
(EventKey (SpecialKey KeyRight) Down _ _) -> ...
(EventKey (SpecialKey KeyLeft) Down _ _) -> ...
_ -> w
where
cellBounds = fromJust $ Map.lookup (playerLocation w) (worldBoundaries w)
Now we'll define a function that will take an access function to the CellBoundaries
. It will determine what our "next" location is.
inputHandler :: Event -> World -> World
inputHandler event w = case event of
...
where
nextLocation :: (CellBoundaries -> BoundaryType) -> Location
nextLocation boundaryFunc = case boundaryFunc cellBounds of
(AdjacentCell cell) -> cell
_ -> playerLocation w
Finally, we pass the proper access function for the bounds with each direction, and we're done!
inputHandler :: Event -> World -> World
inputHandler event w = case event of
(EventKey (SpecialKey KeyUp) Down _ _) ->
w { playerLocation = nextLocation upBoundary }
(EventKey (SpecialKey KeyDown) Down _ _) ->
w { playerLocation = nextLocation downBoundary }
(EventKey (SpecialKey KeyRight) Down _ _) ->
w { playerLocation = nextLocation rightBoundary }
(EventKey (SpecialKey KeyLeft) Down _ _) ->
w { playerLocation = nextLocation leftBoundary }
_ -> w
where
...
Tidying Up
Now we can put everything together in our main function with a little bit of glue.
main :: IO ()
main = play
windowDisplay
white
20
(World (0, 0) (0,0) (24,24) (boundariesMap (25, 25)))
(drawingFunc (globalXOffset, globalYOffset) globalCellSize)
inputHandler
updateFunc
updateFunc :: Float -> World -> World
updateFunc _ = id
Note that for now, we don't have much of an "update" function. Our world doesn't change over time. Yet! We'll see in the coming weeks what other features we can add that will make use of this.
Conclusion
So we've finished stage 1 of our simple game! You can explore the part-1
branch on our Github repository to look at the code if you want! Come back next week and we'll explore how we can actually create a true maze, instead of an open grid. This will involve some interesting algorithmic challenges!
For some more ideas of advanced Haskell libraries, check out our Production Checklist. You can also read our Web Skills Series for a more in-depth tutorial on some of those ideas.
Making a Glossy Game! (Part 1)
I've always had a little bit of an urge to try out game development. It wasn't something I associated with Haskell in the past. But recently, I started learning a bit about game architecural patterns. I stumbled on some ideas that seemed "Haskell-esque". I learned about the Entity-Component-System model, which suits typeclasses rather than object-oriented design.
So I've decided to do a few articles on writing a basic game in Haskell. We'll delve more into these architectural ideas later in the series. But to start, we have to learn a few building blocks! The first couple weeks will focus on the basics of the Gloss library. This library has some simple tools for creating 2D graphics that we can use to make a game. Frequent readers of this blog will note a lot of commonalities between Gloss and the Codeworld library we studied a while back. In this first part, we'll learn some basic combinators.
If you're looking for some more practical usages of Haskell, we have some tools for you! Download our Production Checklist to learn many interesting libraries you can use! You can also read our Haskell Web Skills series to go a bit more in depth!
A Basic Gloss Tutorial
The get started with the Gloss library, let's draw a simple picture using the display
function. All this does is make a full screen window with a circle in the middle.
-- Always imported
import Graphics.Glass
main :: IO ()
main = display FullScreen white (Circle 80)
All the arguments here are pretty straightforward. The program opens a full screen window and displays a circle against a white background. We can make the window smaller by using InWindow
instead of FullScreen
for the Display
type. This takes a window "name", as well as dimensions for the size and offset of the window.
windowDisplay :: Display
windowDisplay = InWindow "Window" (200, 200) (10, 10)
main :: IO ()
main = display windowDisplay white (Circle 80)
The primary argument here is this last one, a Picture
using the Circle
constructor. We can draw many different things, including circles, lines, boxes, text, and so on. The Picture
type also allows for translation, rotation, and aggregation of other pictures.
Animating
We can take our drawing to the next level by using the animate
function. Instead of only drawing a static picture, we'll take the animation time as an input to a function. Here's how we can provide an animation of a growing circle:
main = animate windowDisplay white animationFunc
animationFunc :: Float -> Picture
animationFunc time = Circle (2 * time)
Simulating
The next stage of our program's development is to add a model. This allows us to add state to our animation so that it is no longer merely a function of the time. For our next example, we'll make a pendulum. We'll keep two pieces of information in our model. These are the current angle ("theta") and the derivative of that angle ("dtheta"). The simulate
function takes more arguments than animate
. Here's the skeleton of how we use it. We'll go over the new arguments one-by-one.
type Model = (Float, Float)
main = simulate
displayWindow
white
simulationRate
initialModel
drawingFunc
updateFunc
where
simulationRate :: Int
simulationRate = 20
initialModel :: Model
initialModel = (0,0)
drawingFunc :: Model -> Picture
drawingFunc (theta, dtheta) = …
updateFunc :: ViewPort -> Float -> Model -> Model
updateFunc _ dt (theta, dtheta) = ...
The first extra argument (simulationRate
) tells us how many model steps per second. Then we have our initial model. Then there's a function taking the model and telling us how to draw the picture. We'll fill this in to draw a line at the appropriate angle.
drawingFunc :: Model -> Picture
drawingFunc (theta, dtheta) = Line [(0, 0), (50 * cos theta, 50 * sin theta)]
Finally, we have an updating function. This takes the view-port, which we won't use. It also takes the amount of time for this simulation step (dt
). Then it takes a current model. It uses these to determine the new model. We can fill this in with a little bit of trigonometry. Then we'll have a working pendulum simulation!
updateFunc :: ViewPort -> Float -> Model -> Model
updateFunc _ dt (theta, dtheta) = (theta + dt * dtheta, dtheta - dt * (cos theta))
Playing a Game
The final element we need to make a playable game is to accept user input. The play
function provides us what we need here. It looks like the simulate
function except for an extra function for handling input. We're going to make a game where the user can move a circle around with the arrow keys. We'll add an extra mechanic where the circle keeps trying to move back towards the center. Here's the skeleton:
type World = (Float, Float)
main :: IO ()
main = play
windowDisplay
white
20
(0, 0)
drawingFunc
inputHandler
updateFunc
drawingFunc :: World -> Picture
drawingFunc (x, y) = ...
inputHandler :: Event -> World -> World
inputHandler event (x, y) = ...
updateFunc :: Float -> World -> World
updateFunc dt (x, y) = ...
Our World
will represent the current location of our circle. The drawing function will draw a simple circle, translated by this amount.
drawingFunc :: World -> Picture
drawingFunc (x, y) = translate x y (Circle 20)
Now for our input handler, we only care about a few inputs. We'll read the up/down/left/right arrows, and adjust the coordinates:
inputHandler :: Event -> World -> World
inputHandler (EventKey (SpecialKey KeyUp) Down _ _) (x, y) = (x, y + 10)
inputHandler (EventKey (SpecialKey KeyDown) Down _ _) (x, y) = (x, y - 10)
inputHandler (EventKey (SpecialKey KeyRight) Down _ _) (x, y) = (x + 10, y)
inputHandler (EventKey (SpecialKey KeyLeft) Down _ _) (x, y) = (x - 10, y)
inputHandler _ w = w
Finally, let's write our "update" function. This will keep trying to move the circle's coordinates towards the center of the frame:
updateFunc :: Float -> World -> World
updateFunc _ (x, y) = (towardCenter x, towardCenter y)
where
towardCenter :: Float -> Float
towardCenter c = if abs c < 0.25
then 0
else if c > 0
then c - 0.25
else c + 0.25
And that's it, we have our miniature game!
Conclusion
Hopefully this article gave you a good, quick overview on the basics of the Gloss library. Next week, we'll start making a more complicated game with a more interesting model!
We have other resources for the more adventurous Haskellers out there! Download our Production Checklist and read our Haskell Web Skills Series!
Purescript IV: Routing and Navigation!
Welcome to the conclusion of our series on Purescript! We've spent a lot of time now learning to use functional languages for frontend web. Last Week we saw how to build a basic UI with Purescript. We made a simple counter and then a todo list application, as we did with Elm. This week, we'll explore two more crucial pieces of functionality. We'll see how to send web requests and how to provide different routes for our application.
There are two resources you can look at if you want more details on how this code works. First, you can look at our Github repository. You can also explore the Halogen Github repository. Take a look at the driver-routing and effects-ajax example.
Web Requests
For almost any web application, you're going to need to retrieve some data from a backend server. We'll use the purescript-affjax library to make requests from our Halogen components. The process is going to be a little simpler than it was with Elm.
In Elm, we had to hook web requests into our architecture using the concept of commands. But Purescript's syntax uses monads by nature. This makes it easier to work effects into our eval
function.
In this first part of the article, we're going to build a simple web UI that will be able to send a couple requests. As with all our Halogen components, let's start by defining our state, message, and query types:
type State =
{ getResponse :: String
, postInfo :: String
}
initialState :: State
initialState =
{ getResponse: "Nothing Yet"
, postInfo: ""
}
data Query a =
SendGet a |
SendPost a |
UpdatedPostInfo String a
data Message = ReceivedFromPost String
We'll store two pieces of information in the state. First, we'll store a "response" we get from calling a get request, which we'll initialize to a default string. Then we'll store a string that the user will enter in a text field. We'll send this string through a post request. We'll make query constructors for each of the requests we'll send. Then, our message type will allow us to update our application with the result of the post request.
We'll initialize our component as we usually do, except with one difference. In previous situations, we used an unnamed m
monad for our component stack. This time, we'll specify the Aff
monad, enabling our asynchronous messages. This monad also gets applied to our eval
function.
webSender :: H.Component HH.HTML Query Unit Message Aff
webSender = H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
render :: State -> H.ComponentHTML Query
…
eval :: Query ~> H.ComponentDSL State Query Message Aff
…
Our UI will have four elements. We'll have a p
field storing the response from our get request, as well as a button for triggering that request. Then we'll have an input field where the user can enter a string. There will also be a button to send that string in a post request. These all follow the patterns we saw in part 3 of this series, so we won't dwell on the specifics:
render :: State -> H.ComponentHTML Query
render st = HH.div [] [progressText, getButton, inputText, postButton]
where
progressText = HH.p [] [HH.text st.getResponse]
getButton = HH.button
[ HP.title "Send Get", HE.onClick (HE.input_ SendGet) ]
[ HH.text "Send Get" ]
inputText = HH.input
[ HP.type_ HP.InputText
, HP.placeholder "Form Data"
, HP.value st.postInfo
, HE.onValueChange (HE.input UpdatedPostInfo)
]
postButton = HH.button
[ HP.title "Send Post", HE.onClick (HE.input_ SendPost) ]
[ HH.text "Send Post" ]
Our eval
function will assess each of the different queries we can receive, as always. When updating the post request info (the text field), we update our state with the new value.
eval :: Query ~> H.ComponentDSL State Query Message Aff
eval = case _ of
SendGet next -> ...
SendPost next -> ...
UpdatedPostInfo newInfo next -> do
st <- H.get
H.put (st { postInfo = newInfo })
pure next
Now let's specify our get
request. The get
function from the Affjax library takes two parameters. First we need a "deserializer", which tells us how to convert the response into some desired type. We'll imagine we're getting a String
back from the server, so we'll use the string
deserializer. The our second parameter is the URL. This will be a localhost address. We call liftAff
to get this Aff
call into our component monad.
import Affjax as AX
import Affjax.ResponseFormat as AXR
eval :: Query ~> H.ComponentDSL State Query Message Aff
eval = case _ of
SendGet next -> do
response <- H.liftAff $ AX.get AXR.string "http://localhost:8081/api/hello"
...
SendPost next -> ...
UpdatedPostInfo newInfo next -> ...
The response contains a lot of information, including things like the status code. But our main concern is the response body. This is an Either
value giving us a success or error value. In either case, we'll put a reasonable value into our state, and call the next action!
eval :: Query ~> H.ComponentDSL State Query Message Aff
eval = case _ of
SendGet next -> do
response <- H.liftAff $ AX.get AXR.string "http://localhost:8081/api/hello"
st <- H.get
case response.body of
Right success -> H.put (st { getResponse = success })
Left _ -> H.put (st { getResponse = "Error!" })
pure next
SendPost next -> ...
UpdatedPostInfo newInfo next -> ...
Then we can go to our UI, click the button, and it will update the field with an appropriate value!
Post Requests
Sending a post request will be similar. The main change is that we'll need to create a body for our post request. We'll do this using the "Argonaut" library for Purescript. The fromString
function gives us a JSON
object. We wrap this into a RequestBody
with the json
function:
import Affjax.RequestBody as AXRB
import Data.Argonaut.Core as JSON
…
eval :: Query ~> H.ComponentDSL State Query Message Aff
eval = case _ of
SendGet next -> ...
SendPost next -> do
st <- H.get
let body = AXRB.json (JSON.fromString st.postInfo)
...
UpdatedPostInfo newInfo next -> ...
Aside from adding this body parameter, the post
function works as the get
function does. We'll break the response body into Right
and Left
cases to determine the result. Instead of updating our state, we'll send a message about the result.
eval :: Query ~> H.ComponentDSL State Query Message Aff
eval = case _ of
SendGet next -> ...
SendPost next -> do
st <- H.get
let body = AXRB.json (JSON.fromString st.postInfo)
response <- H.liftAff $ AX.post AXR.string "http://localhost:8081/api/post" body
case response.body of
Right success -> H.raise (ReceivedFromPost success)
Left _ -> H.raise (ReceivedFromPost "There was an error!")
pure next
UpdatedPostInfo newInfo next -> ...
And that's the basics of web requests!
Routing Basics
Now let's change gears and consider how we can navigate among different pages. For the sake of example, let's say we've got 4 different types of pages in our app.
- A home page
- A login page
- A user profile page
- A page for each article
Each user profile will have an integer user ID attached to it. Each article will have a string identifier attached to it as well as a user ID for the author. Here's a traditional router representation of this:
/home
/login
/profile/:userid
/blog/articles/:userid/:articleid
With the Purescript Routing library, our first step is to represent our set of routes with a data type. Each route will represent a page on our site, so we'll call our type Page
. Here's how we do that:
data Page =
HomePage |
LoginPage |
ProfilePage Int |
ArticlePage Int String
By using a data structure, we'll be able to ensure two things. First, all the routes in our application have some means of handling them. If we're missing a case, the compiler will let us know. Second, we'll ensure that our application logic cannot route the user to an unknown page. We will need to use one of the routes within our data structure.
Building a Parser
That said, the user could still enter any URL they want in the address bar. So we have to know how to parse URLs into our different pages. For this, we have to build a parser on our route type. This will have the type Match Page
. This will follow an applicative parsing structure. For more background on this, check out this article from our parsing series!
But even if you've never seen this kind of parsing before, the patterns aren't too hard. The first thing to know is that the lit
function (meaning literal) matches a string path component. So we feed it the string element we want, and it will match our route.
For our home page route, we'll want to first match the URL component "home".
import Routing.Match (Match, lit, int, str)
matchHome = lit "home"
But this will actually give us a Match
that outputs a String
. We want to ignore the string we parsed, and give a constructor of our Page
type. Here's what that looks like:
matchHome :: Match Page
matchHome = HomePage <$ lit "home"
The <$
data-preserve-html-node="true" operator tells us we want to perform a functor wrap. Except we want to ignore the resulting value from the second part. This gives our first match!
The login page will have a very similar matcher:
matchLogin :: Match Page
matchLogin = LoginPage <$ lit "login"
But then for the profile page, we'll actually want to use the result from one of our matchers! We want to use int
to read the integer out of the URL component and plug it into our data structure. For this, we need the applicative operator <*>
. Except once again, we'll have a string part that we ignore, so we'll actually use *>
. Here's what it looks like:
matchProfile :: Match Page
matchProfile = ProfilePage <$> (lit "profile" *> int)
Now for our final matcher, we'll keep using these same ideas! We'll use the full applicative operator <*>
since we want both the user ID and the article ID.
matchArticle :: Match Page
matchArticle = ArticlePage <$>
(lit "blog" *> lit "articles" *> int) <*> string
Now we combine our different matchers into a router by using the <|>
operator from Alternative
:
router :: Match Page
router = matchHome <|> matchLogin <|> matchProfile <|> matchArticle
And we're done! Notice how similar Purescript and Haskell are in this situation! Pretty much all the code from this section could work in Haskell. (As long as we used the corresponding libraries).
Incorporating Our Router
Now to use this routing mechanism, we're going to need to set up our application in a special way. It will have one single parent component and several child components. We will make it so that our application can listen to changes in the URL. We'll use our router to match those changes to our URL scheme. Our parent component will, as always, respond to queries.
We won't go through the details of our child components. You can take a look at src/NavComponents.purs
in our Github repo for details there.
We'll use some special mechanisms to send a query on each route change event. Then our parent component will handle updating the view. An important thing to know is that all the child components have the same query and message type. We won't use these much in this article, but these are how you would customize app-wide behavior.
type ChildState = Int
data ChildQuery a = ChildQuery a
data ChildMessage = ChildMessage
Each child component will have a link to the "next page" in the sequence. This way, we can show how these links work once we render it. We'll need access to these component definitions in our parent module:
homeComponent :: forall m.
H.Component HH.HTML ChildQuery Unit ChildMessage m
loginComponent :: forall m.
H.Component HH.HTML ChildQuery Unit ChildMessage m
profileComponent :: forall m. Int ->
H.Component HH.HTML ChildQuery Unit ChildMessage m
articleComponent :: forall m. Int -> String ->
H.Component HH.HTML ChildQuery Unit ChildMessage m
The Parent Component
Now let's start our by making a simple query type for our parent element. We'll have one query for changing the page, and one for processing messages from our children.
data ParentQuery a =
ChangePage Page a |
HandleAppAction Message a
The parent's state will include the current page. It could also include some secondary elements like the ID of the logged in user, if we wanted.
type ParentState = { currentPage :: Page }
Now we'll need slot designations for the "child" element of our page. Depending on the state of our application, our child element will be a different component. This is how we'll represent the different pages of our application.
data SlotId = HomeSlot | LoginSlot | ProfileSlot | ArticleSlot
Our eval
and render
functions should be pretty straightforward. When we evaluate the "change page" query, we'll update our state. Then we won't do anything when processing a ChildMessage
:
eval :: forall m. ParentQuery ~>
H.ParentDSL ParentState ParentQuery ChildQuery SlotId Void m
eval = case _ of
ChangePage pg next -> do
H.put {currentPage: pg}
pure next
HandleAppAction _ next -> do
pure next
For our render function, we first need a couple helpers. The first goes from the page to the slot ID. The second gives a mapping from our page data structure to the proper component.
slotForPage :: Page -> SlotId
slotForPage HomePage = HomeSlot
slotForPage LoginPage = LoginSlot
slotForPage (ProfilePage _) = ProfileSlot
slotForPage (ArticlePage _ _) = ArticleSlot
componentForPage :: forall m. Page ->
H.Component HH.HTML ChildQuery Unit Message m
componentForPage HomePage = homeComponent
componentForPage LoginPage = loginComponent
componentForPage (ProfilePage uid) = profileComponent uid
componentForPage (ArticlePage uid aid) = articleComponent uid aid
Now we can construct our render function. We'll access the page from our state, and then create an appropriate slot
for it:
render :: forall m. ParentState ->
H.ParentHTML ParentQuery ChildQuery SlotId m
render st = HH.div_
[ HH.slot sl comp unit (HE.input HandleAppAction)
]
where
sl = slotForPage st.currentPage
comp = componentForPage st.currentPage
Adding Routing
Now to actually apply the routing in our application, we'll update our Main
module. This process will be a little complicated. There are a lot of different libraries involved in reading event changes. We won't dwell too much on the details, but here's the high level overview.
Every time the user changes the URL or clicks a link, this produces a HashChangeEvent
. We want to create our own Producer
that will listen for these events so we can send them to our application. Here's what that looks like:
import Control.Coroutine as CR
import Control.Coroutine.Aff as CRA
import Web.HTML (window) as DOM
import Web.HTML.Event.HashChangeEvent as HCE
import Web.HTML.Event.HashChangeEvent.EventTypes as HCET
hashChangeProducer :: CR.Producer HCE.HashChangeEvent Aff Unit
hashChangeProducer = CRA.produce \emitter -> do
listener <- DOM.eventListener
(traverse_ (CRA.emit emitter) <<< HCE.fromEvent)
liftEffect $
DOM.window
>>= Window.toEventTarget
>>> DOM.addEventListener HCET.hashchange listener false
Now we want our application to consume these events. So we'll set up a Consumer
function. It consumes the hash change events and passes them to our UI, as we'll see:
hashChangeConsumer
:: (forall a. ParentQuery a -> Aff a)
-> CR.Consumer HCE.HashChangeEvent Aff Unit
hashChangeConsumer query = CR.consumer \event -> do
let hash = Str.drop 1 $ Str.dropWhile (_ /= '#') $ HCE.newURL event
result = match router hash
newPage = case result of
Left _ -> HomePage
Right page -> page
void $ liftAff $ query $ H.action (ChangePage newPage)
pure Nothing
There are a couple things to notice. We drop the hash up until the #
to get the relevant part of our URL. Then we pass it to our router
for processing. Finally, we pass an appropriate ChangePage
action to our UI.
How do we do this? Well, the first argument of this consumer function (query
) is actually another function. This function takes in our ParentQuery
and produces an Aff
event. We can access this function as a result of the runUI
function.
So our final step is to run our UI. Then we run a separate process that will chain the producer and consumer together:
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
io <- runUI parentComponent unit body
CR.runProcess (hashChangeProducer CR.$$ hashChangeConsumer io.query)
We pass the io.query
property of our application UI to the consumer, so our UI can react to the events. And now our application will respond to URL changes!
Conclusion
This wraps up our series on Purescript! Between this and our Elm Series , you should have a good idea on how to use functional languages to write a web UI. As a reminder, you can see more details on running Purescript code on our Github Repository. The README will walk you through the basic steps of getting this code setup.
You can also take a look at some of our other resources on web development using Haskell! Read our Haskell Web Series to see how to write a backend for your application. You can also download our Production Checklist to learn about more libraries you can use.
Purescript III: Making a Web Page with Purescript and React!
Last week we continued learning the basic elements of Purescript. We examined how typeclasses and monads work and the slight differences from Haskell. Now it's finally time to use Purescript for its main purpose: frontend web development. We'll accomplish this using the Halogen framework, built on React.js.
In this article, we'll learn about the basic concepts of Halogen/React. We'll build a couple simple components to show how these work. Next week, we'll conclude our look at Purescript by making a more complete application. We'll see how to handle routing and sending web requests.
If you're building a frontend, you'll also need a backend at some point. Check out our Haskell Web Series to learn how to do that in Haskell!
Also, getting Purescript to work can be tricky business! Take a look at our Github repository for some more setup instructions!
Halogen Crash Course
The Halogen framework uses React.js under the hood, and the code applies similar ideas. If you don't do a lot of web development, you might not be too familiar with the details of React. Luckily, there are a few simple principles we'll apply that will remind us of Elm!
With Halogen, our UI consists of different "components". A component is a UI element that maintains its own state and properties. It also responds to queries, and sends messages. For any component, we'll start by defining a a state type, a query type, and a message type.
data CState = …
data CQuery = …
data CMessage = ...
Our component receives queries from within itself or from other components. It can then send messages to other components, provided they have queries to handle them. With these types in place, we'll use the component
function to define a component with 3 main elements. As a note, we'll be maintaining these import prefixes throughout the article.
import Halogen as H
import Halogen.HTML as HH
import Halogen.Events as HE
import Halogen.Properties as HP
myComponent :: forall m.
H.Component HH.HTML CQuery Unit CMessage m
myComponent = H.component
{ initialState: …
, render: …
, eval: …
, receiver: const Nothing
}
where
render ::
CState ->
H.ComponentHTML CQuery
eval ::
CQuery ~>
H.ComponentDSL CState CQuery CMessage m
The initialState
is self explanatory. The render
function will be a lot like our view
function from Elm. It takes a state and returns HTML components that can send queries. The eval
function acts like our update
function in Elm. Its type signature looks a little strange. But it takes queries as inputs and can update our state using State
monad function. It can also emit messages to send to other components.
Building a Counter
For our first example of a component, we'll make a simple counter. We'll have an increment button, a decrement button and a display of the current count. Our state will be a simple integer. Our queries will involve events from incrementing and decrementing. We'll also send a message each time we update our number.
type State = Int
data Query a =
Increment a |
Decrement a
data Message = Updated Int
Notice we have an extra parameter on our query type. This represents the "next" action that will happen in our UI. We'll see how this works when we write our eval
function. But first, let's write out our render
function. It has three different HTML elements: two buttons and a p
label. We'll stick them in a div
element.
render :: State -> H.ComponentHTML Query
render state =
let incButton = HH.button
[ HP.title "Inc"
, HE.onClick (HE.input_ Increment)
]
[ HH.text "Inc" ]
decButton = HH.button
[ HP.title "Dec"
, HE.onClick (HE.input_ Decrement)
]
[ HH.text "Dec" ]
pElement = HH.p [] [HH.text (show state)]
in HH.div [] [incButton, decButton, pElement]
Each of our elements takes two list parameters. The first list includes properties as well as event handlers. Notice our buttons send query messages on their click events using the input_
function. Then the second list is "child" HTML elements, including the inner text of a button.
Now, to write our eval
function, we use a case statement. This might seem a little weird, but all we're doing is breaking it down into our query cases:
eval :: Query ~> H.ComponentDSL State Query Message m
eval = case _ of
Increment next -> ...
Decrement next -> ...
Within each case, we can use State
monad-like functions to manipulate our state. Our cases are identical except for the sign. We'll also use the raise
function to send an update message. Nothing listens for that message right now, but it illustrates the concept.
eval :: Query ~> H.ComponentDSL State Query Message m
eval = case _ of
Increment next -> do
state <- H.get
let nextState = state + 1
H.put nextState
H.raise $ Updated nextState
pure next
Decrement next -> do
state <- H.get
let nextState = state - 1
H.put nextState
H.raise $ Updated nextState
pure next
As a last note, we would use const 0
as the initialState
in our component function.
Installing Our Component
Now to display this component in our UI, we write a short Main
module like so. We get our body element with awaitBody
and then use runUI
to install our counter component.
module Main where
import Prelude
import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import Counter (counter)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI counter unit body
And our counter component will now work! (See Github for more details on you could run this code).
Building Our Todo List
Now that we've got the basics down, let's see how to write a more complicated set of components. We'll write a Todo list like we had in the Elm series. To start, let's make a Todo
wrapper type and derive some instances for it:
newtype Todo = Todo
{ todoName :: String }
derive instance eqTodo :: Eq Todo
derive instance ordTodo :: Ord Todo
Our first component will be the entry form, where the user can add a new task. This form will use the text input string as its state. It will respond to queries for updating the name as well as pressing the "Add" button. When we create a new Todo, we'll send a message for that.
type AddTodoFormState = String
data AddTodoFormMessage = NewTodo Todo
data AddTodoFormQuery a =
AddedTodo a |
UpdatedName String a
When we render this component, we'll have two main pieces. First, we need the text field to input the name. Then, there's the button to add the task. Each of these has an event attached to it sending the relevant query. In the case of updating the name, notice we use input
instead of input_
. This allows us to send the text field's value as an argument of the UpdatedName
query. Otherwise, the properties are pretty straightforward translations of HTML properties you might see.
render ::
AddTodoFormState ->
H.ComponentHTML AddTodoFormQuery
render currentName =
let nameInput = HH.input
[ HP.type_ HP.InputText
, HP.placeholder "Task Name"
, HP.value currentName
, HE.onValueChange (HE.input UpdatedName)
]
addButton = HH.button
[ HP.title "Add Task"
, HP.disabled (length currentName == 0)
, HE.onClick (HE.input_ AddedTodo)
]
[ HH.text "Add Task" ]
in HH.div [] [nameInput, addButton]
Evaluating our queries is pretty simple. When updating the name, all we do is update the state and trigger the next action. When we add a new Todo item, we save the empty string as the state and raise our message. In the next part, we'll see how our list will respond to this message.
eval ::
AddTodoFormQuery ~>
H.ComponentDSL
AddTodoFormState AddTodoFormQuery AddTodoFormMessage m
eval = case _ of
AddedTodo next -> do
currentName <- H.get
H.put ""
H.raise $ NewTodo (Todo {todoName: currentName})
pure next
UpdatedName newName next -> do
H.put newName
pure next
And of course, we tie this all up by using the component
function:
addTodoForm :: forall m.
H.Component HH.HTML AddTodoFormQuery Unit AddTodoFormMessage m
addTodoForm = H.component
{ initialState: const ""
, render
, eval
, receiver: const Nothing
}
Finishing the List
Now to complete our todo list, we'll need another component to store the tasks themselves. As always, let's start with our basic types. We won't bother with a message type since this component won't send any messages. We'll use Void
when assigning the message type in a type signature:
type TodoListState = Array Todo
data TodoListQuery a =
FinishedTodo Todo a |
HandleNewTask AddTodoFormMessage a
Our state is our list of tasks. Our query type is a little more complicated. The HandleNewTask
query will receive the new task messages from our form. We'll see how we make this connection below.
We'll also add a type alias for AddTodoFormSlot
. Halogen uses a "slot ID" to distinguish between child elements. We only have one child element though, so we'll use a string.
type AddTodoFormSlot = String
We'll consider this component a "parent" of our "add task" form. This means the types will look a little different. We'll be making something of type ParentHTML
. The type signature will include references to its own query type, the query type of its child, and the slot ID type. We'll still use most of the same functions though.
render ::
TodoListState ->
H.ParentHTML TodoListQuery AddTodoFormQuery AddTodoFormSlot m
eval ::
TodoListQuery ~>
H.ParentDSL TodoListState TodoListQuery AddTodoFormQuery
AddTodoFormSlot Void m
To render our elements, we'll have two sub-components. First, we'll want to be able to render an individual Todo within our list. We'll give it a p
label for the name and a button that completes the task:
renderTask ::
Todo ->
H.ParentHTML TodoListQuery AddTodoFormQuery AddTodoFormSlot m
renderTask (Todo t) = HH.div_
[ HH.p [] [HH.text t.todoName]
, HH.button
[ HE.onClick (HE.input_ (FinishedTodo (Todo t)))]
[HH.text "Finish"]
]
Now we need some HTML for the form slot itself. This is straightforward. We'll use the slot
function and provide a string for the ID. We'll specify the component we have from the last part. Then we'll attach the HandleNewTask
query to this component. The allows our list component to receive the new-task messages from the form.
formSlot ::
H.ParentHTML TodoListQuery AddTodoFormQuery AddTodoFormSlot m
formSlot = HH.slot
"Add Todo Form"
addTodoForm
unit
(HE.input HandleNewTask)
Now we combine these elements in our render
function:
render ::
TodoListState ->
H.ParentHTML TodoListQuery AddTodoFormQuery AddTodoFormSlot m
render todos =
let taskList = HH.ul_ (map renderTask todos)
in HH.div_ [taskList, formSlot]
Writing our eval
is now a simple matter of using a few array functions to update the list. When we get a new task, we add it to our list. When we finish a task, we remove it from the list.
eval ::
TodoListQuery ~>
H.ParentDSL TodoListState TodoListQuery AddTodoFormQuery
AddTodoFormSlot Void m
eval = case _ of
FinishedTodo todo next -> do
currentTasks <- H.get
H.put (filter (_ /= todo) currentTasks)
pure next
HandleNewTask (NewTodo todo) next -> do
currentTasks <- H.get
H.put (currentTasks `snoc` todo)
pure next
And that's it! We're done! Again, take a look at the Github repo for some more instructions on how you can run and interact with this code.
Conclusion
This wraps up our look at building simple UI's with Purescript. Next week we'll conclude our Purescript series. We'll look at some of the broader elements of building a web app. We'll see some basic routing as well as how to send requests to a backend server.
Elm is another great functional language you can use for Web UIs. To learn more about it, check out our recently concluded Elm Series!
Purescript II: Typeclasses and Monads
Last week, we started our exploration of Purescript. Purescript seeks to bring some of the awesomeness of Haskell to the world of web development. Its syntax looks a lot like Haskell's, but it compiles to Javascript. This makes it very easy to use for web applications. And it doesn't just look like Haskell. It uses many of the important features of the language, such as a strong system and functional purity.
If you need to brush up on the basics of Purescript, make sure to check out that first article again. This week, we're going to explore a couple other areas where Purescript is a little different. We'll see how Purescript handles type-classes, and we'll also look at monadic code. We'll also take a quick look at some other small details with operators. Next week, we'll look at how we can use Purescript to write some front-end code.
For another perspective on functional web development, check out our Haskell Web Series. You can also download our Production Checklist for some more ideas! You can also take a gander at our Elm Series for another frontend language!
Type Classes
The idea of type classes remains pretty consistent from Haskell to Purescript. But there are still a few gotchas. Let's remember our Triple
type from last week.
data Triple = Triple
{ a :: Int
, b :: Int
, c :: Int
}
Let's write a simple Eq
instance for it. To start with, instances in Purescript must have names. So we'll assign the name tripleEq
to our instance:
instance tripleEq :: Eq Triple where
eq (Triple t1) (Triple t2) = t1 == t2
Once again, we only unwrap the one field for our type. This corresponds to the record, rather than the individual fields. We can, in fact, compare the records with each other. The name we provide helps Purescript to generate Javascript that is more readable. Take note: naming our instances does NOT allow us to have multiple instances for the same type and class. We'll get a compile error if we try to create another instance like:
instance otherTripleEq :: Eq Triple where
...
There's another small change when using an explicit import for classes. We have to use the class
keyword in the import list:
import Data.Eq (class Eq)
You might hope we could derive the Eq
typeclass for our Triple
type, and we can. Since our instance needs a name though, the normal Haskell syntax doesn't work. The following will fail:
-- DOES NOT WORK
data Triple = Triple
{ a :: Int
, b :: Int
, c :: Int
} deriving (Eq)
For simple typeclasses though, we CAN use standalone deriving. This allows us to provide a name to the instance:
derive instance eqTriple :: Eq Triple
As a last note, Purescript does not allow orphan instances. An orphan instance is where you define a typeclass instance in a different file from both the type definition and the class definition. You can get away with these in Haskell, though GHC will warn you about it. But Purescript is less forgiving. The way to work around this issue is to define a newtype wrapper around your type. Then you can define the instance on that wrapper.
Effects
In part 1, we looked at a small snippet of monadic code. It looked like:
main :: Effect Unit
main = do
log ("The answer is " <> show answer)
If we're trying to draw a comparison to Haskell, it seems as though Effect
is a comparable monad to IO
. And it sort've is. But it's a little more complicated than that. In Purescript, we can use Effect
to represent "native" effects. Before we get into exact what this means and how we do it, let's first consider "non-native" effects.
A non-native effect is one of those monads like Maybe
or List
that can stand on its own. In fact, we have an example of the List
monad in part 1 of this series. Here's what Maybe
might look like.
maybeFunc :: Int -> Maybe Int
mightFail :: Int -> Maybe Int
mightFail x = do
y <- maybeFunc x
z <- maybeFunc y
maybeFunc z
Native effects use the Effect
monad. These include a lot of things we'd traditionally associate with IO
in Haskell. For instance, random number generation and console output use the Effect
monad:
randomInt :: Int -> Int -> Effect Int
log :: String -> Effect Unit
But there are also other "native effects" related to web development. The most important of these is anything that writes to the DOM in our Javascript application. Next week, we'll use the purescript-react
library to create a basic web page. Most of its main functions are in the Effect
monad. Again, we can imagine that this kind of effect would use IO
in Haskell. So if you want to think of Purescript's Effect
as an analogue for IO
, that's a decent starting point.
What's interesting is that Purescript used to be more based on the system of free monads. Each different type of native effect would build on top of previous effects. The cool part about this is the way Purescript uses its own record syntax to track the effects in play. You can read more about how this can work in chapter 8 of the Purescript Book. However, we won't need it for our examples. We can just stick with Effect
.
Besides free monads, Purescript also has the purescript-transformers
library. If you're more familiar with Haskell, this might be a better starting spot. It allows you to use the MTL style approach that's more common in Haskell than free monads.
Special Operators
It's worth noting a couple other small differences. Some rules about operators are a little different between Haskell and Purescript. Since Purescript uses the period operator .
for record access, it no longer refers to function composition. Instead, we would use the <<<
operator:
odds :: List Int -> List Int
odds myList = filter (not <<< isEven) myList
where
isEven :: Int -> Boolean
isEven x = mod x 2 == 0
Also, we cannot define operators in an infix way. We must first define a normal name for them. The following will NOT work:
(=%=) :: Int -> Int -> Int
(=%=) a b = 2 * a - b
Instead, we need to define a name like addTwiceAndSubtract
. Then we can tell Purescript to apply it as an infix operator:
addTwiceAndSubtract :: Int -> Int -> Int
addTwiceAndSubtract a b = 2 * a - b
infixrl 6 addTwiceAndSubtract as =%=
Finally, using operators as partial functions looks a little different. This works in Haskell but not Purescript:
doubleAll :: List Int -> List Int
doubleAll myList = map (* 2) myList
Instead, we want syntax like this:
doubleAll :: List Int -> List Int
doubleAll myList = map (_ * 2) myList
Conclusion
This wraps up our look at the key differences between Haskell and Purescript. Now that we understand typeclasses and monads, it's time to dive into what Purescript is best at. Come back next week, and we'll look at how we can write real frontend code with Purescript!
For some more ideas on using Haskell for some cool functionality, download our Production Checklist! For another look at function frontend development, check out our recent Elm Series!
Getting Started with Purescript!
Our Haskell Web Series covers a lot of cool libraries you can use when making a web app. But one thing we haven't covered on this blog yet is using Haskell for front-end web development. There are a number libraries and frameworks out there. Yesod and Snap come to mind. Another option is Reflex FRP, which uses GHCJS under the hood.
But for this new series I've decided to take a different approach. For the next few weeks, we're going to be exploring the Purescript language. Purescript is a bit of a meld between Haskell and Javascript. Its syntax is like Haskell's, and it incorporates many elements of functional purity. But it compiles to Javascript and thus has some features that seem more at home in that language.
This week, we'll start out by exploring the basics of Purescript. We'll see some of the main similarities and differences between it and Haskell. We'll culminate this series by making a web front-end with Purescript. We'll connect this front-end to Haskell code on the back-end.
Purescript is the tip of the iceberg when it comes to using functional languages in product! Check out our Production Checklist for some awesome Haskell libraries!
Getting Started
Since Purescript is its own language, we'll need some new tools. You can follow the instructions on the Purescript website, but here are the main points.
- Install Node.js and NPM, the Node.js package manager
- Run
npm install -g purescript
- Run
npm install -g pulp bower
- Create your project directory and run
pulp init
. - You can then build and test code with
pulp build
andpulp test
. - You can also use PSCI as a console, similar to GHCI.
First, we need NPM. Purescript is its own language, but we want to compile it to Javascript we can use in the browser, so we need Node.js. Then we'll globally install the Purescript libraries. We'll also install pulp
and bower
. Pulp will be our build tool like Cabal.
Bower is a package repository like Hackage. To get extra libraries into our program, you would use the bower
command. For instance, we need purescript-integers
for our solution later in the article. To get this, run the command:
bower install --save purescript-integers
A Simple Example
Once you're set up, it's time to start dabbling with the language. While Purescript compiles to Javascript, the language itself actually looks a lot more like Haskell! We'll examine this by comparison. Suppose we want to find the all pythagorean triples whose sum is less than 100. Here's how we can write this solution in Haskell:
sourceList :: [Int]
sourceList = [1..100]
allTriples :: [(Int, Int, Int)]
allTriples =
[(a, b, c) | a <- sourceList, b <- sourceList, c <- sourceList]
isPythagorean :: (Int, Int, Int) -> Bool
isPythagorean (a, b, c) = a ^ 2 + b ^ 2 == c ^ 2
isSmallEnough :: (Int, Int, Int) -> Bool
isSmallEnough (a, b, c) = a + b + c < 100
finalAnswer :: [(Int, Int, Int)]
finalAnswer = filter
(\t -> isPythagorean t && isSmallEnough t)
allTriples
Let's make a module in Purescript that will allow us to solve this same problem. We'll start by writing a module Pythagoras.purs
. Here's the code we would write to match up with the Haskell above. We'll examine the specifics piece-by-piece below.
module Pythagoras where
import Data.List (List, range, filter)
import Data.Int (pow)
import Prelude
sourceList :: List Int
sourceList = range 1 100
data Triple = Triple
{ a :: Int
, b :: Int
, c :: Int
}
allTriples :: List Triple
allTriples = do
a <- sourceList
b <- sourceList
c <- sourceList
pure $ Triple {a: a, b: b, c: c}
isPythagorean :: Triple -> Boolean
isPythagorean (Triple triple) =
(pow triple.a 2) + (pow triple.b 2) == (pow triple.c 2)
isSmallEnough :: Triple -> Boolean
isSmallEnough (Triple triple) =
(triple.a) + (triple.b) + (triple.c) < 100
finalAnswer :: List Triple
finalAnswer = filter
(\triple -> isPythagorean triple && isSmallEnough triple)
allTriples
For the most part, things are very similar! We still have expressions. These expressions have type signatures. We use a lot of similar elements like lists and filters. On the whole, Purescript looks a lot more like Haskell than Javascript. But there are some key differences. Let's explore those, starting with the higher level concepts.
Differences
One difference you can't see in code syntax is that Purescript is NOT lazily evaluated. Javascript is an eager language by nature. So it is much easier to compile to JS by starting with an eager language in the first place.
But now let's consider some of the differences we can see from the code. For starters, we have to import more things. Purescript does not import a Prelude
by default. You must always explicitly bring it in. We also need imports for basic list functionality.
And speaking of lists, Purescript lacks a lot of the syntactic sugar Haskell has. For instance, we need to use List Int
rather than [Int]
. We can't use ..
to create a range, but instead resort to the range
function.
We also cannot use list comprehensions. Instead, to generate our original list of triples, we use the list monad. As with lists, we have to use the term Unit
instead of ()
:
-- Comparable to main :: IO ()
main :: Effect Unit
main = do
log "Hello World!"
Next week, we'll discuss the distinction between Effect
in Purescript and monadic constructs like IO
in Haskell.
One annoyance is that polymorphic type signatures are more complicated. Whereas in Haskell, we have no issue creating a type signature [a] -> Int
, this will fail in Purescript. Instead, we must always use the forall
keyword:
myListFunction :: forall a. List a -> Int
Another thing that doesn't come up in this example is the Number
type. We can use Int
in Purescript as in Haskell. But aside from that the only important numeric type is Number
. This type can also represent floating point values. Both of these get translated into the number
type in Javascript.
Purescript Data Types
But now let's get into one of the more glaring differences between our examples. In Purescript, we need to make a separate Triple
type, rather than using a simple 3-tuple. Let's look at the reasons for this by considering data types in general.
If we want, we can make Purescript data types in the same way we would in Haskell. So we could make a data type to represent a Pythagorean triple:
data Triple = Triple a b c
This works fine in Purescript. But, it forces us to use pattern matching every time we want to pull an individual value out of this element. We can fix this in Haskell by using record syntax to give ourselves accessor functions:
data Triple = Triple
{ a :: Int
, b :: Int
, c :: Int
}
This syntax still works in Purescript, but it means something different. In Purescript a record is its own type, like a generic Javascript object. For instance, we could do this as a type synonym and not a full data type:
type Triple = { a :: Int, b :: Int, c :: Int}
oneTriple :: Triple
oneTriple = { a: 5, b: 12, c: 13}
Then, instead of using the field names like functions, we use "dot-syntax" like in Javascript. Here's what that looks like with our type synonym definition:
type Triple = { a :: Int, b :: Int, c :: Int}
oneTriple :: Triple
oneTriple = { a: 5, b: 12, c: 13}
sumAB :: Triple -> Int
sumAB triple = triple.a + triple.b
Here's where it gets confusing though. If we use a full data type with record syntax, Purescript no longer treats this as an item with 3 fields. Instead, we would have a data type that has one field, and that field is a record. So we would need to unwrap the record using pattern matching before using the accessor functions.
data Triple = Triple
{ a :: Int
, b :: Int
, c :: Int
}
oneTriple :: Triple
oneTriple = Triple { a: 5, b: 12, c: 13}
sumAB :: Triple -> Int
sumAB (Triple triple) = triple.a + triple.b
-- This is wrong!
sumAB :: Triple -> Int
sumAB triple = triple.a + triple.b
That's a pretty major gotcha. The compiler error you get from making this mistake is a bit confusing, so be careful!
Pythagoras in Purescript
With this understanding, the Purescript code above should make some more sense. But we'll go through it one more time and point out the little details.
To start out, let's make our source list. We don't have the range syntactic sugar, but we can still use the range
function:
import Data.List (List, range, filter)
data Triple = Triple
{ a :: Int
, b :: Int
, c :: Int
}
sourceList :: List Int
sourceList = range 1 100
We don't have list comprehensions. But we can instead use do-syntax with lists instead to get the same effect. Note that to use do-syntax in Purescript we have to import Prelude
. In particular, we need the bind
function for that to work. So let's generate all the possible triples now.
import Prelude
…
allTriples :: List Triple
allTriples = do
a <- sourceList
b <- sourceList
c <- sourceList
pure $ Triple {a: a, b: b, c: c}
Notice also we use pure
instead of return
. Now let's write our filtering functions. These will use the record pattern matching and accessing mentioned above.
isPythagorean :: Triple -> Boolean
isPythagorean (Triple triple) =
(pow triple.a 2) + (pow triple.b 2) == (pow triple.c 2)
isSmallEnough :: Triple -> Boolean
isSmallEnough (Triple triple) =
(triple.a) + (triple.b) + (triple.c) < 100
Finally, we can combine it all with filter
in much the same way we did in Haskell:
finalAnswer :: List Triple
finalAnswer = filter
(\triple -> isPythagorean triple && isSmallEnough triple)
allTriples
And now our solution will work!
Conclusion
This week we started our exploration of Purescript. Syntactically, Purescript is a very near cousin of Haskell. But there are a few key differences we highlighted here about the nature of the language.
Next week, we'll look at some other important differences in the type system. We'll see how Purescript handles type-classes and monads. After that, we'll see how we can use Purescript to build a web front-end with some of the security of a solid type system.
Download our Production Checklist for some more cool ideas of libraries you can use!
Elm IV: Navigation!
Last week, we learned a few more complexities about how Elm works. We examined how to bridge Elm types and Haskell types using the elm-bridge
library. We also saw a couple basic ways to incorporate effects into our Elm application. We saw how to use a random generator and how to send HTTP requests.
These forced us to stretch our idea of what our program was doing. Our original Todo application only controlled a static page with the sandbox
function. But this new program used element
to introduce effects into our program structure.
But there's still another level for us to get to. Pretty much any web app will need many pages, and we haven't seen what navigation looks like in Elm. To conclude this series, let's see how we incorporate different pages. We'll need to introduce a couple more components into our application for this.
Simple Navigation
Now you might be thinking navigation should be simple. After all, we can use normal HTML elements on our page, including the a
element for links. So we'd set up different HTML files in our project and use routes to dictate which page to visit. Before Elm 0.19, this was all you would do.
But this approach has some key performance weaknesses. Clicking a link will always lead to a page refresh which can be disrupting for the user. This approach will also lead us to do a lot of redundant loading of our library code. Each new page will have to reload the generated Javascript for Data.String
, for example. The latest version of Elm has a new solution for this that fits within the Elm architecture.
An Application
In our previous articles, we described our whole application using the element
function. But now it's time to evolve from that definition. The application
function provides us the tools we need to build something bigger. Let's start by looking at its type signature (see the appendix at the bottom for imports):
application :
{ init : flags -> Url -> Key -> (model, Cmd msg)
, view : model -> Document msg
, update : msg -> model -> (model, Cmd msg)
, subscriptions : model -> Sub msg
, onUrlRequest : UrlRequest -> msg
, onUrlChange : Url -> msg
}
- > Program flags model msg
There are a couple new fields to this application function. But we can start by looking at the changes to what we already know. Our init
function now takes a couple extra parameters, the Url
and the Key
. Getting a Url
when our app launches means we can display different content depending on what page our users visit first. The Key
is a special navigation tool we get when our app starts that helps us in routing. We need it for sending our own navigation commands.
Our view
and update
functions haven't really changed their types. All that's new is the view
produces Document
instead of only Html
. A Document
is a wrapper that lets us add a title to our web page, nothing scary. The subscriptions
field has the same type (and we'll still ignore it for the most part).
This brings us to the new fields, onUrlRequest
and onUrlChange
. These intercept events that can change the page URL. We use onUrlChange
to update our page when a user changes the URL at the top bar. Then we use onUrlRequest
to deal with a
links the user clicks on the page.
Basic Setup
Let's see how all these work by building a small dummy application. We'll have three pages, arbitrarily titled "Contents", "Intro", and "Conclusion". Our content will just be a few links allowing us to navigate back and forth. Let's start off with a few simple types. For our program state, we store the URL so we can configure the page we're on. We also store the navigation key because we need it to push changes to the page. Then for our messages, we'll have constructors for URL requests and changes:
type AppState = AppState
{ url: Url
, navKey : Key
}
type AppMessage =
NoUpdate |
ClickedLink UrlRequest |
UrlChanged Url
When we initialize this application, we'll pass the URL and Key through to our state. We'll always start the user at the contents page. We cause a transition with the pushUrl
command, which requires we use the navigation key.
appInit : () -> Url -> Key -> (AppState, Cmd AppMessage)
appInit _ url key =
let st = AppState {url = url, navKey = key}
in (st, pushUrl key "/contents")
Updating the URL
Now we can start filling in our application
. We've got message types corresponding to the URL requests and changes, so it's easy to fill those in.
main : Program () AppState AppMessage
main = Browser.application
{ init : appInit
, view = appView
, update = appUpdate
, subscriptions = appSubscriptions
, onUrlRequest = ClickedLink -- Use the message!
, onUrlChanged = UrlChanged
}
Our subscriptions, once again, will be Sub.none
. So we're now down to filling in our update
and view
functions.
The first real business of our update
function is to handle link clicks. For this, we have to break the UrlRequest
down into its Internal
and External
cases:
appUpdate : AppMessage -> AppState -> (AppState, Cmd AppMessage)
appUpdate msg (AppState s) = case msg of
NoUpdate -> (AppState s, Cmd.none)
ClickedLink urlRequest -> case urlRequest of
Internal url -> …
External href -> ...
Internal requests go to pages within our application. External requests go to other sites. We have to use different commands for each of these. As we saw in the initialization, we use pushUrl
for internal requests. Then external requests will use the load
function from our navigation library.
appUpdate : AppMessage -> AppState -> (AppState, Cmd AppMessage)
appUpdate msg (AppState s) = case msg of
NoUpdate -> (AppState s, Cmd.none)
ClickedLink urlRequest -> case urlRequest of
Internal url -> (AppState s, pushUrl s.navKey (toString url))
External href -> (AppState s, load href)
Once the URL has changed, we'll have another message. The only thing we need to do with this one is update our internal state of the URL.
appUpdate : AppMessage -> AppState -> (AppState, Cmd AppMessage)
appUpdate msg (AppState s) = case msg of
NoUpdate -> (AppState s, Cmd.none)
ClickedLink urlRequest -> …
UrlChanged url -> (AppState {s | url = url}, Cmd.None)
Rounding out the View
Now our application's internal logic is all set up. All that's left is the view! First let's write a couple helper functions. The first of these will parse our URL into a page so we know where we are. The second will create a link element in our page:
type Page =
Contents |
Intro |
Conclusion |
Other
parseUrlToPage : Url -> Page
parseUrlToPage url =
let urlString = toString url
in if contains "/contents" urlString
then Contents
else if contains "/intro" urlString
then Intro
else if contains "/conclusion" urlString
then Conclusion
else Other
link : String -> Html AppMessage
link path = a [href path] [text path]
Finally let's fill in a view function by applying these:
appView : AppState -> Document AppMessage
appView (AppState st) =
let body = case parseUrlToPage st.url of
Contents -> div []
[ link "/intro", br [] [], link "/conclusion" ]
Intro -> div []
[ link "/contents", br [] [], link "/conclusion" ]
Conclusion -> div []
[ link "/intro", br [] [], link "/contents" ]
Other -> div [] [ text "The page doesn't exist!" ]
in Document "Navigation Example App" [body]
And now we can navigate back and forth among these pages with the links!
Conclusion
In this last part of our series, we completed the development of our Elm skills. We learned how to use an application
to achieve the full power of a web app and navigate between different pages. There's plenty more depth we can get into with designing an Elm application. For instance, how do you structure your message types across your different pages? What kind of state do you use to manage your user's experience. We'll explore these another time.
We're not done with functional frontend yet though! We've got another series coming up that'll teach you the basics of Purescript. So stay tuned for that!
And you'll also want to make sure your backend skills are up to snuff as well! Read our Haskell Web Series for more details on that! You can also download our Production Checklist!
Appendix: Imports
import Browser exposing (application, UrlRequest(..), Document)
import Browser.Navigation exposing (Key, load, pushUrl)
import Html exposing (button, div, text, a, Html, br)
import Html.Attributes exposing (href)
import Html.Events exposing (onClick)
import String exposing (contains)
import Url exposing (Url, toString)
Elm III: Adding Effects
Last week we dug deeper into using Elm. We saw how to build a more complicated web page forming a Todo list application. We learned about the Elm architecture and saw how we could use a couple simple functions to build our page. We laid the groundwork for bringing effects into our system, but didn't use any of these.
This week, we'll add some useful pieces to our Elm skill set. We'll see how to include more effects in our system, specifically randomness and HTTP requests.
To learn more about constructing a backend for your system, you should read up on our Haskell Web Series. It'll teach you things like connecting to a database and making an HTTP server.
Incorporating Effects
Last week, we explored using the element
expression to build our application. Unlike sandbox
, this allowed us to add commands, which enable side effects. But we didn't use any of commands. Let's examine a couple different effects we can use in our application.
One simple effect we can cause is to get a random number. It might not be obvious from the code we have so far, but we can't actually do it in our Todo application at the moment! Our update
function is pure! This means it doesn't have access to IO
. What it can do is send commands as part of its output. Commands can trigger messages, and incorporate effects along the way.
Making a Random Task
We're going to add a button to our application. This button will generate a random task name and add it to our list. To start with, we'll add a new message type to process:
type TodoListMessage =
AddedTodo Todo |
FinishedTodo Todo |
UpdatedNewTodo (Maybe Todo) |
AddRandomTodo
Now here's the HTML element that will send the new message. We can add it to the list of elements in our view:
randomTaskButton : Html TodoListMessage
randomTaskButton = button [onClick AddRandomTodo] [text "Random"]
Now we need to add our new message to our update function. We need a case for it:
todoUpdate : TodoListMessage -> TodoListState -> (TodoListState, Cmd TodoListMessage)
todoUpdate msg (TodoListState { todoList, newTodo}) = case msg of
…
AddRandomTodo ->
(TodoListState { todoList = todoList, newTodo = newTodo}, …)
So for the first time, we're going to fill in the Cmd
element! To generate randomness, we need the generate
function from the Random
module.
generate : (a -> msg) -> Generator a -> Cmd msg
We need two arguments to use this. The second argument is a random generator on a particular type a
. The first argument then is a function from this type to our message. In our case, we'll want to generate a String
. We'll use some functionality from the package elm-community/random-extra
. See Random.String and Random.Char for details. Our strings will be 10 letters long and use only lowercase.
genString : Generator String
genString = string 10 lowerCaseLatin
Now we can easily convert this to a new message. We generate the string, and then add it as a Todo:
addTaskMsg : String -> TodoListMessage
addTaskMsg name = AddedTodo (Todo {todoName = name})
Now we can plug these into our update function, and we have our functioning random command!
todoUpdate : TodoListMessage -> TodoListState -> (TodoListState, Cmd TodoListMessage)
todoUpdate msg (TodoListState { todoList, newTodo}) = case msg of
…
AddRandomTodo ->
(..., generate addTaskMsg genString)
Now clicking the random button will make a random task and add it to our list!
Sending an HTTP Request
A more complicated effect we can add is to send an HTTP request. We'll be using the Http
library from Elm. Whenever we complete a task, we'll send a request to some endpoint that contains the task's name within its body.
We'll hook into our current action for FinishedTodo
. Currently, this returns the None
command along with its update. We'll make it send a command that will trigger a post request. This post request will, in turn, hook into another message type we'll make for handling the response.
todoUpdate : TodoListMessage -> TodoListState -> (TodoListState, Cmd TodoListMessage)
todoUpdate msg (TodoListState { todoList, newTodo}) = case msg of
…
(FinishedTodo doneTodo) ->
(..., postFinishedTodo doneTodo)
ReceivedFinishedResponse -> ...
postFinishedTodo : Todo -> Cmd TodoListMessage
postFinishedTodo = ...
We create HTTP commands using the send
function. It takes two parameters:
send : (Result Error a -> msg) -> Request a -> Cmd Msg
The first of these is a function interpreting the server response and giving us a new message to send. The second is a request expecting a result of some type a
. Let's plot out our code skeleton a little more for these parameters. We'll imagine we're getting back a String
for our response, but it doesn't matter. We'll send the same message regardless:
postFinishedTodo : Todo -> Cmd TodoListMessage
postFinishedTodo todo = send interpretResponse (postRequest todo)
interpretResponse : Result Error String -> TodoListMessage
interpretResposne _ = ReceivedFinishedResponse
postRequest : Todo -> Request String
postRequest = ...
Now all we need is to create our post request using the post
function:
post : String -> Body -> Decoder a -> Request a
Now we've got three more parameters to fill in. The first of these is the URL we're sending the request to. The second is our body. The third is a decoder for the response. Our decoder will be Json.Decode.string
, a library function. We'll imagine we are running a local server for the URL.
postRequest : Todo -> Request String
postRequest todo = post "localhost:8081/api/finish" … Json.Decode.string
All we need to do now is encode the Todo in the post request body. This is straightforward. We'll use Json.Encode.object
, which takes a list of tuples. Then we'll use the string
encoder on the todo name.
jsonEncTodo : Todo -> Value
jsonEncTodo (Todo todo) = Json.Encode.object
[ ("todoName", Json.encode.string todo.todoName) ]
Now all we have to do is use this encoder together with the jsonBody
function. And then we're done!
postRequest : Todo -> Request String
postRequest todo = post
"localhost:8081/api/finish"
(jsonBody (jsonEncTodo todo))
Json.Decode.string
As a reminder, most of the types and helper functions from this last part come from the HTTP Library for Elm. We could then further process the response in our interpretResponse
function. If we get an error, we could send a different message. Either way, we can ultimately do more updates in our update
function.
Conclusion
This concludes part 3 of our series on Elm! We took a look at a few nifty ways to add extra effects to our Elm projects. We saw how to introduce randomness into our Elm project, and then how to send HTTP requests. Next week, we'll explore navigation, a vital part of any web application. We'll see how the Elm architecture supports a multi-page application. Then we'll see how to move between the different pages efficiently, without needing to reload every bit of our Elm code each time.
Now that you know how to write a functional frontend, you should learn more about the backend! Read our Haskell Web Series for some tutorials on how to do this. You can also download our Production Checklist for some more ideas!