Is there a way to make sure I don’t forget to add a parser for a new Route when I extend the type with a new constructor?
This is the best I could come up with, which is not very typesafe:
parser : Parser (Route -> a) a
parser =
Url.Parser.oneOf
[ handlers.signup.urlParser
, handlers.login.urlParser
, handlers.refresh.urlParser
, handlers.logout.urlParser
, handlers.refreshAnonymous.urlParser
, handlers.attack.urlParser
]
{-| This is only here to make sure I don't forget to add the new route's parser.
-}
parserFailsafe : Route -> String
parserFailsafe route =
case route of
NotFound ->
"yes I have added the new parser to `parser` above"
Signup auth ->
"yes I have added the new parser to `parser` above"
Refresh ->
"yes I have added the new parser to `parser` above"
RefreshAnonymous ->
"yes I have added the new parser to `parser` above"
Login auth ->
"yes I have added the new parser to `parser` above"
Attack attackData ->
"yes I have added the new parser to `parser` above"
Logout ->
"yes I have added the new parser to `parser` above"
How about make a function getParser : Route -> Parser a.
parser =
Url.Parser.oneOf (List.map getParser allRoutes)
-- this ensures that a parser exists for the type.
getParser : Route -> Parser a
getParser route =
case route of
NotFound ->
handler.notfound.urlparser
Signup auth ->
handler.signup.urlparser
Refresh ->
handler.refresh.urlparser
RefreshAnonymous ->
handler.refreshanonymous.urlparser
Login auth ->
handler.login.urlparser
Attack attackData ->
handler.attack.urlparser
Logout ->
handler.logout.urlparser
-- you still have to remember to put your type here!
allRoutes =
[ NotFound
, Signup
, Refresh
, RefreshAnonymous
, Login
, Attack
, Logout
]
I had a sudden thought on this! Ok you’d still have to use fake args for your types that require them, and there’s an additional function to maintain. But you get an automatically generated list of all the types!
nextType : Route -> Maybe Route
nextType : route =
case route of
NotFound -> Just Signup
Signup -> Just Refresh
Refresh -> Just RefreshAnonymous
RefreshAnonymous -> Just Login
Login -> Just Attack
Attack -> Just Logout
Logout -> Nothing
makeAllRoutes : Maybe Route -> List Route
makeAllRoutes mbroute =
case mbroute of
Nothing -> []
Just route -> route :: makeAllRoutes (nextType route)
allRoutes : List Routes
allRoutes = makeAllRoutes NotFound
It’d be nice if the compiler were able to detect missing branches in (opted-in?) oneOf s. It could benefit Routing, Json, or all kinds of parsing/data building… Not sure it’s feasible (because order can be critical, some are values others are functions…), but it’d be a big step toward ensuring all possible states are covered.
I think the general case here would be something like, “How do I have the compiler guarantee that every constructor of a Type is included in an enumerable?”
I don’t believe that is possible in elm, thus I don’t believe there is any way to guarantee what you are asking for without creating the enumerable yourself, but at that point you’ve not much gain over the standard way of route parsing.
Thank you, this worked! Yeah it’s a bit more work, but it uses case ... of in the process and thus reminds me to plug the new constructor in “the chain”.
Ultimately what is another hurdle is that it is rather common to represent a Route multiple times (like e.g. common misspellings, have the singular word redirect to the plural word, or allow route-names in multiple languages), so that’s one pattern that you cannot use at the same time as using the approach @progger proposes.
By the way, is there a way in ‘debug’ Elm to get a list of all the different data constructors? Testing an URL parser is the kind of thing that might be worthwhile (since we lack type-level lists) to do using QuickCheck/Fuzzing… as long as those tests can of course be generated automatically based on the actual type that is constructed. (Because of course, having to manually maintain an URL fuzzer on top of the URL parser and the URL builder is not useful).
There are a lot of interesting blog posts talking about how to do URL parsing (and/or building) in Haskell, but most of these realize that some type-level trickery really is useful here: