A simple page

Purescript Halogen

Halogen is based on a collection of components each maintaining its own state.

Lets create a simple app based on a single component, and then take it from there. Our App is going to be a very basic web page that asks someone their name and then says hello.

Demo

Setup

First setup the project as per here.

Code

Ok, let’s write some code. If you open ./src/Main.purs you will see Spago has given us a simple initial main function.

module Main where

import Prelude

import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "🍝"

Imports

Change the imports to the following, to import the various modules we need to create our control:

import Prelude
import Data.Const (Const)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)

With Purescript we can import every symbol implicitly from a module. We are doing this with Prelude. Prelude gives us a lot of default functionality that pretty much every project will use and is generally just imported by default every time. Unlike in Haskell which automatically imports Prelude, in Purescript it must be done explicitly. Note that if you import more than module implicitly, Purescript will warn you. It is only really intended for the Prelude to be implicitly imported, every other module needs to be qualified. (It gets really confusing knowing what came from where if you aren’t explicit.)

We can specify the specific symbols to import. We are importing Const from the Data.Const module. We are also importing the Maybe symbol and it’s constructors (hence the (..) specified after Maybe) from the Data.Maybe module.

We can import a module qualified. So we are importing the Halogen module qualified with H. So if we use anything from the Halogen module we have to prefix it with an H., eg. calling the mkComponent function from Halogen is called as H.mkComponent.

Types

Now we need to set up some data types for our component. This is the beauty of using a strongly typed language - if you don’t specify a type for it, you can’t use it!

The first type is the State record. Every Halogen component maintains some internal state that it uses to know how to display itself and do generally useful things. For our component, the only thing we need to keep track of is the name that is being entered so we know who to say hello to.

type State = { name :: String }

This sets up a simple record with a single field called name that is a String. Our name will never ever be able to be a number, or a Dom object.. or anything other than a String. Also our components state is guaranteed to have a name. If we try to access anything else, we will not be able to compile.

Our next type is the Action type. This type specifies all the different events that our component will be able to respond to. It is only in responing to these events that the component will be able to update its state. We have to be explicit about what it is our component can do.

Our component can do one thing.

data Action = UpdateName String

We can update a name. Note that UpdateName takes a string parameter. Our component can only update a name if it has a valid String that it can update the name to.

Component

We are ready to setup our component.

page :: forall m. H.Component HH.HTML (Const Void) Unit Void m 
page = 
    H.mkComponent { initialState: const initialState
                  , render
                  , eval: H.mkEval $ H.defaultEval
                    { handleAction = handleAction }
                  }

page is a function that returns the created component. Its type spec is a tad gnarly. Lets look at it a bit closer.

Component is a type defined (slightly simplified) by the Halogen library as a structure that takes 5 different parameters :

data Component h f i o m 
  • h specifies the type that is rendered by the component. We are rendering HTML.
  • f specifies the type a parent component can use to query this component.
  • i is the type of any data used to initialize this component.
  • o is the data this component can use to send messages to the parent component.
  • m is the monad that the component uses to update its state, and invoke any other effects (such as querying an http server) during its update cycle.

Since we have nothing worth querying we just specify (Const Void) as the f parameter. Void is a type that can never be created, so by specifying Void here we are saying that it is impossible to query us. (Let’s ignore the Const bit for now, just accept that it needs to be there..)

For our i initialization parameter we specify Unit. Whilst Void can never be created, Unit can - but it can only ever have one value - unit is the only allowed value for the Unit type. What we are saying here is that there is nothing that you can really pass to us to initialize us, so we are going to be ignoring it.

Likewise we send no messages to our parent, so again this is Void.

Since we don’t want to tie down our component to running in any particular Monad, we just specify forall m.. This comes in very handy later as we can setup our own custom monad to handle our more complicated effects. The type spec can be enhanced to say that we will take any m as long as for example, it allows us to send a particular Http request.

Our component is created with the mkComponent function of Halogen. We pass in a record that sets out the different functions that will manage our components lifecycle.

initialState

Since we are just ignoring any initialization parameters from the parent we just set up our initialState to be const initialState. const just ignores the second parameter passed to it, so effectively takes the initialState function and returns a new function that ignores its parameter.

initialState :: State
initialState = { name: "" }

The initialState function has to return the State type we set up early, thus forcing us to ensure the component is initialized to some known state. Here we just set up with an empty name.

handleAction

Our handleAction function is the core engine of our components functionality. Here we respond to messages and update our state.

handleAction :: forall m. Action -> H.HalogenM State Action () Void m Unit
handleAction ( UpdateName newName ) = 
    H.modify_ _{ name = newName }

The type definition is suitably gnarly. We take one parameter - the Action structure we defined early and we return H.HalogenM State Action () Void m Unit

HalogenM is a core Monad of the Halogen framework. Defined as newtype HalogenM s f g p o m a it gives us the following parameters :

  • s The state type of our component. On our page it is the State data structure.
  • f The action structure that specifies the event we are handling. This is the Action structure.
  • g Is the type of our child slots. We don’t have any child components, so in this example we just pass ().
  • p Not sure yet.
  • o Not sure.
  • m The main monad that the update runs in. This is the same m as the m defined for the overall component.
  • a The return type of the handle function. Since we don’t return anything, this is just Unit.

We only have one action that needs to be handled - UpdateName. This action contains a parameter with the name that we want to update our components state to. We can do this with the Halogen modify_ function. Now of course, since Purescript is a pure language, modify_ does not actually change the state. We will pass a function that takes the old state and returns a new object with the state that we want it to be. This new object becomes the new state of the component. The old state is often just garbage collected, but we could keep it around if we wanted to. (The canonical example is that of keeping track of all our states so that we can implement some kind of Undo buffer.)

We pass _{ name = name } to modify_. This is Purescript’s syntactic sugar for an anonymous function. It expands to something like :

(\parameter -> parameter { name = newName })

The _ just takes the place of the first parameter passed to the function. parameter { name = newName } says, take all the fields in the parameter record and pass me a copy of it with the following fields (in this case, the name field) replaced with the given value.

render

The render function is the function that takes our components state and generates the appropriate Html.

render :: forall m. State -> H.ComponentHTML Action () m
render state =
    HH.div_ [ HH.p_ [ HH.text "What is your name?" ]
            , HH.input [ HP.type_ HP.InputText
                       , HE.onValueInput $ Just <<< UpdateName
                       ]
            , hello
            ]

    where
      hello = if state.name == ""
              then HH.p_ []
              else HH.p_ [ HH.text $ "Hello " <> state.name ]

The type of the function takes a State record containing the state of the component and returns ComponentHTML action slots m.

  • action This is the action structure that we can use to invoke events to be handled by our handleAction function.
  • slots This is the data type to represent any child components we have. We have no children, so this is just ().
  • m The monad.

Our Html is built up programmatically. Halogen provides a bunch of functions, each one creates a particular Html element. Most of these functions will take two array parameters. The first parameter is a list of properties for the element. The second is a list of the child elements. As we have imported the Halogen.HTML module qualified with HH, we prefix our calls with HH. For many of the elements Halogen also provides function suffixed with _ that only takes a single parameter for the child elements list, for those cases where our element has no properties.

You should see a fairly close mapping between the generated Html and your function calls. The beauty is that your code is strongly typed so it is very hard, if not impossible to generate invalid Html. Take a look at the HP.type_ parameter entered for HH.input. This has to take a parameter of type InputType. This type only contains valid Input types.

The HE.onValueInput is an event handler that gets invoked by the browser when the user changes the value of our input field. This property takes a parameter of (String -> Maybe i) where i is a value of our Action data structure - this becomes the next event that gets sent to the handleAction. The return of this function is a Maybe. We have the option of returning Nothing if we don’t actually want to invoke any Action, or Just action if we do.

In a longer form we could write the function as :

 HE.onValueInput $ (\newName -> Just ( UpdateName newName) )

Using the function composition operator <<< we can rewrite that as :

 HE.onValueInput (\newName -> (Just <<< UpdateName) newName) 

And then since all functions in Purescript are curried, we can just lose the parameter and Purescript will know it is expecting one, giving us :

 HE.onValueInput (Just <<< UpdateName) 

We can lose the parens and just put a $ there instead :

 HE.onValueInput $ Just <<< UpdateName

As our render function is pure Purescript code, we can use Purescript functions as normal. The hello function checks that we have a name available and either returns an empty paragraph or a paragraph that says “Hello” plus the name. We don’t want to say hello to nobody. We can then call this function as we are building up the child elements for the main div as normal.

Main

Now all we need to do to wrap up our control is to display it. Lets create a main function to render our component.

main :: Effect Unit
main = HA.runHalogenAff $
       HA.awaitBody >>= runUI page unit

The main function returns Effect Unit. Because Purescript is a pure language it is impossible for a function to return a different result when given the same parameters. It is also impossible for a function to perform any side effects. (Purescript takes this very seriously!) However a program that doesn’t have any side effects is pretty useless. So what Purescript does is provide us with this Effect type (known as the Effect Monad). This type effectively allows us to tell the runtime which commands we would like to run to perform these side effects. The run time takes this sequence of commands and runs them for us, performing these side effects.

The interesting thing is that there is no way to extract the value from the Effect Monad. So, for example if you load some data from Local Storage, there is no way to get this data out. That would mean that you could call a function that would return a different value depending on the contents of the storage, which is forbidden in a pure language.

This sounds useless, but what you can do instead is pass the Monad a function that can be run against the data in the Effect.

Let’s look at our main function. The first call HA.runHalogenAff is the function that kicks off the Halogen process. This returns an Effect Unit. (Unit means there is no value to extract from the Monad - just perform the side effects.

The parameter HA.runHalogenAff takes is forall x. Aff x. The Aff Monad is a wrapper around Effect and thus it is very similar to the Effect monad - it also allows for asynchronous effects to be performed. The forall x. part is saying that we will take any Aff monad and we don’t care what type of value this Monad contains.

HA.awaitBody waits for the HTML document to finish loading and then it returns the body HTML element in the Aff monad. Since this is a side effect (and it returns a different body element every time it loads a different page) it is returned in the Aff monad. We cannot pull this body out of the Monad. Instead we can pass a function to the Monad to be called with this element passed as a parameter. >>=, also known as bind is how you pass the function.

The function we are passing to bind is runUI. This is the function that builds the UI and runs the update cycle that keeps the UI operating.

So, there we have it, a very simple control in Purescript Halogen.

You can find the source code here