Typesafe Url.Parser.oneOf usage?


#1

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"

`enumerate` function for non-infinite custom types - proposal
A tool to generate a list of custom type constructors
#2

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
   ]

#3

Right, but it’s a bit unwieldy if your routes take parameters. And it’s still no better as far as “having to remember to do something” goes…


#4

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

#5

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.


#6

I don’t think this is possible to check statically with Elm’s type system.

This would be possible with polymorphic variants which are kind of like extensible rows for union (or variant, or ‘custom’ types).

Using polymorphic variants, if you have some function which has return type:

[< 'Route1 | 'Route2 | 'Route3 ]

And you attempt to use it with some regular union type which does not have these tags or less, it would be a type error.

So both of these wold be ok:

type Routes = Route1 | Route2

type Routes = Route1 | Route2 | Route3

but this would fail

type Routes = Route1 | Route2 | Route3 | Route4


#7

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.


#8

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”.


#9

Related to this discussion is How to represent that a set of data is complete in a type-safe manner, which, unfortunately, also did not have a really nice conclusion (at least at the time of writing).

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.


#10

Oh, good point! Yes, that would be a problem. In that regard the approach from my original question is probably more flexible.


#11

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:


closed #12

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.