In the last post we created some simple routing for our Halogen app. But as always happens, the boss comes along and wants you to make some enhancements. We now need to add the ability to click on a particular potato to get some interesting information about that potato.
The route for the potato is going to be
#potato/<potato name>. This makes just matching on strings more complicated as we need to pull the potato name out as a parameter. Luckily the routing library contains some parser combinators that make this straightforward.
We’ll start with the code from the previous post. You can download the code from here if you need.
We need to install the routing library. So in the command line cd to the project directory:
> spago install routing Installation complete.
src/Router.purs add the following imports:
import Control.Alt ((<|>)) import Routing (match) import Routing.Match (Match, lit, str, end) import Data.Foldable (oneOf)
We need to add the Potato route to our list of routes:
data Route = Roast | Chips | Salad | Potato String | Dud
Note that this route takes a String parameter. This represents the variety that we need to display the information for.
We have also added another route -
Dud. This route will be called if we cannot match the route. It’s best to show a 404 error rather than just default to
Now we need to create a parser that will match the route. Parsing uses the Match object to match each component in the path. We build up a Match object to represent the routes we want to match with and then pass it to the match function to run the match against a route.
Let’s play with it in the repl to get an idea of how it can work.
PSCi, version 0.13.5 Type :? for help > import Prelude > import Routing > import Routing.Match > import Data.Either >
The most basic matches is against an empty string:
> match end "" (Right unit)
We can also match against the root path:
> match root "/" (Right unit)
We can match against a specific string literal:
> match (lit "potato") "potato" (Right unit)
match returns an either. If the match succeeds it returns
Right. If it fails it returns
Left like so:
> match (lit "potato") "turnip" (Left "expected path part: potato;\n")
Our combinators so far don’t return any data beyond just the fact that they have matched. Other combinators do return data:
> match str "potato" (Right "potato") > match int "42" (Right 42)
Match implements a number of useful typeclasses that we can use to build up our route matching routine.
Match is a
Functor, we can use this to map the values returned by match. Let’s create a quick data structure to represent some routes:
> data Route = Zork | Mong | Nonk String > derive instance eqRoute :: Eq Route >
(We derive an eq instance, so we can test the return values. It’s a bit annoying deriving Show instances in PureScript.)
We can then use
<$ which is infix for the
voidRight function. This function will take a value and a Functor and will replace the value contained in the Functor with the given value.
> match (Zork <$ lit "zork") "zork" == Right Zork true > match (Zork <$ lit "zork") "not zork" == Right Zork false
<$ is not just useful for parsing. It can be used anywhere you have a
> import Data.Maybe > 3 <$ Just 9 (Just 3) > ("I am a sausage" <$ Right 9) :: Either Int String (Right "I am a sausage") > ("I am a sausage" <$ Left 9) :: Either Int String (Left 9) > 7 <$ [1,9,2,4] [7,7,7,7]
As a slight aside, when I said
<$ replaces the value contained in the Functor, I slightly lied. Note that calling it on
Left 9 didn’t change the value at all. This is because Either is only a Functor on its
Right. So no value was changed. Also calling it on an array actually replaced 4 values.
We also have our favorite
<$> that will map the value returned by eg.
str into our Route data.
> match (Nonk <$> str) "somekindanonk" == Right (Nonk "somekindanonk") true
Match also implements
Alt which allows us to specify multiple matches, choosing from the first one that succeeds:
> import Control.Alternative > match (Zork <$ lit "zork" <|> Mong <$ lit "mong") "zork" == Right Zork true > match (Zork <$ lit "zork" <|> Mong <$ lit "mong") "mong" == Right Mong true > isLeft $ match (Zork <$ lit "zork" <|> Mong <$ lit "mong") "cactus" true
The final example fails with
Left as neither of the alternatives matched.
Many routes will have multiple segments.
Match will match multiple segments and it implements
Apply to allow us to match against each one.
> match (lit "ook" *> lit "onk") "ook/onk" (Right unit) > match (lit "ook" *> lit "onk") "ook/ugh" (Left "expected path part: onk;\n")
*> operator - applySecond will run whatever is on the left, ignore any result from that and then run whatever is on the right - returning the result from the right. It does this as long as we are running in the Apply context - so with an object that implements the
Apply so we are good to go.
Now, run means different things depending on how the actual object has been implemented.
Maybe also implements
> import Data.Maybe > Nothing *> Just 7 Nothing > Just 3 *> Just 9 (Just 9)
In the case of
Maybe, the way it runs is to check that it has a value. In the first case we have
Nothing, so the run terminates and
Nothing is returned. This is similar to what would happen if we do not have a match for
In the second case, we do have a value (
Just 3), so the run succeeds and moves on to the next case. This is
Just 9 and that is what is returned. Note that the value from
Just 3 is ignored, it had served its purpose.
Given that we can start building up our routes. Lets go through the various possible routes we would like to match.
pure Roast <* end
This matches an empty route. If it matches it will return
Roast. This gives us a way of defaulting to Roasts if no route is specified.
Roast <$ lit "roast" <* end
We match if we have a route of
roast. And we map this to return
Chips <$ lit "chips" <* end
Salad <$ lit "salad" <* end
Now for potatoes. The potato route contains an extra segment which will contain the name of the potato.
Potato <$> ( lit "potato" *> str <* end )
We match against the literal
potato and then apply (
str which will return the string value of the next path segment. This value is then mapped into
Our final route is a catch all which will match against everything not already matched:
We can put it together using
routing :: Match Route routing = pure Roast <* end <|> Roast <$ lit "roast" <* end <|> Chips <$ lit "chips" <* end <|> Salad <$ lit "salad" <* end <|> Potato <$> ( lit "potato" *> str <* end ) <|> pure Dud
getRoute function needs to be adjusted to match the route using our new Match:
getRoute :: Effect (Maybe Route) getRoute = do hash <- getHash pure $ hush $ match routing hash
As a bonus, the
routing library provides is with a
getHash function that returns the hash from the browser url.
The router function can also be simplified as
routing provides us with a
matches function which will keep track of our route changes and call provided callback each time.
router :: (Route -> Effect Unit) -> Effect Unit router cb = void $ matches routing (\_old new -> cb new)
The call back passes two parameters, the route we are moving away from (if there is one) and the new route we are moving to. We just ignore the old route and call the original callback with the new route.
matches returns an effect we can use to remove the listener if we no longer want to track the route. We don’t need this for this app, so we just ignore it with
There we have it. Once you get used to using these combinators it is trivial to handle some very complex routing requirements in a typesafe manner - if you get the matching code wrong so it isn’t able to match a route according to how you have set it up in the data structure, it will fail to compile.
You can find the sample code here.