Parse the "rest" of a URL

#1

Hi

I am currently porting one of the projects I used to learn Elm from 0.18 to 0.19, it has mostly been a breeze, but I have ran into one problem.

In my application I had got some help form the slack to make a URL parser that would take the “rest” of a given URL after a keyword and give it pack as a list of strings:

For example, In the given URL, everything after album belongs together, and should be given as a list:
example.com/#/album/content/root/index.json

So we will have “album” and [“content”, “root”, “index.json”]

This way I could pass the list as an argument to the album page which then new where to get the data it needed.

The solution I had was a recursive URL parse “builder” with a max depth that would sort this out for me. For credits sake, I basically got help with the whole ting on slack, and tried to understand it.

The code looks like this, and works in 0.18:

In the oneOf:

UrlParser.map (Album << String.join "/") (s "album" </> rest)

And the parser:

rest : UrlParser.Parser (List String -> a) a
rest =
    restHelp 10


restHelp : Int -> UrlParser.Parser (List String -> a) a
restHelp maxDepth =
    if maxDepth < 1 then
        UrlParser.map [] UrlParser.top
    else
        UrlParser.oneOf
            [ UrlParser.map [] UrlParser.top
            , UrlParser.map (::) (UrlParser.string </> restHelp (maxDepth - 1))
            ]

I have rewritten the function to use elm/url, and now it looks like this:

rest : Parser.Parser (List String -> a) a
rest =
    restHelp 10


restHelp : Int -> Parser.Parser (List String -> a) a
restHelp maxDepth =
    if maxDepth < 1 then
        Parser.map [] Parser.top

    else
        Parser.oneOf
            [ Parser.map [] Parser.top
            , Parser.map (\str li -> str :: li) (Parser.string </> restHelp (maxDepth - 1))
            ]

This however does not seem to work, and I get a type mismatch that I cannot figure out:

------------------- src/Data/Url.elm

The 2nd element of this list does not match all the previous elements:

51|             [ Parser.map [] Parser.top
52|>            , Parser.map (\str li -> str :: li) (Parser.string </> restHelp (maxDepth - 1))
53|             ]

This `map` call produces:

    Parser.Parser (a -> c) c

But all the previous elements in the list are:

    Parser.Parser (List a -> c) c

-- TYPE MISMATCH ---------------------------------------------- src/Data/Url.elm

The 2nd argument to `map` is not what I expect:

52|             , Parser.map (\str li -> str :: li) (Parser.string </> restHelp (maxDepth - 1))
                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This argument is:

    Parser.Parser (String -> List String -> a) a

But `map` needs the 2nd argument to be:

    Parser.Parser (String -> List String -> List String) a

Does anyone have any suggestions on how to solve this? I am up for a different solution also as I feel like it might be a bit hacky.

I was thinking about the though of something that parse strings until it hits a json file with a custom encoder; but cannot seem to get that to work either…

For example ending with a match of:

json : Parser.Parser (String -> a) a
json =
    Parser.custom "JSON_FILE" <|
        \segment ->
            if String.endsWith ".json" segment then
                Just segment

            else
                Nothing

Thank you.

#2

Though I admit I can’t give a great explanation of this, I think the recursive nature of your restHelp function is not compatible with the type variables.

I tried changing only your type annotations on rest and restHelp, making all the a type variables into List String instead, and it compiles:

rest : Parser.Parser (List String -> List String) (List String)
rest =
    restHelp 10


restHelp : Int -> Parser.Parser (List String -> List String) (List String)
restHelp maxDepth =
    if maxDepth < 1 then
        Parser.map [] Parser.top

    else
        Parser.oneOf
            [ Parser.map [] Parser.top
            , Parser.map (\str li -> str :: li) (Parser.string </> restHelp (maxDepth - 1))
            ]

Here’s an example in the REPL that seems to show that it’s at least headed in the right direction:

> Maybe.andThen (Url.Parser.parse Main.rest) (Url.fromString "http://x.y.c/apple/book/cat/dog")
Just ["apple","book","cat","dog"]
    : Maybe (List String)

Hope this helps. And if someone wants to add a nice explanation of what’s going on with the types here, I would certainly be interested in understanding it better.

#3

Since I find this to be an interesting case to learn more about the Elm type system and type inference, I’ve continued trying to understand what’s going on here.

Here’s an alternative approach I’ve found that compiles/works while keeping the expected generalized type:

rest : Parser.Parser (List String -> a) a
rest =
    restHelpE 10


restHelpO : Int -> Parser.Parser (List String -> a) a
restHelpO maxDepth =
    if maxDepth < 1 then
        Parser.map [] Parser.top

    else
        Parser.oneOf
            [ Parser.map [] Parser.top
            , Parser.map (\str li -> str :: li) (Parser.string </> restHelpE (maxDepth - 1))
            ]


restHelpE : Int -> Parser.Parser (List String -> a) a
restHelpE maxDepth =
    if maxDepth < 1 then
        Parser.map [] Parser.top

    else
        Parser.oneOf
            [ Parser.map [] Parser.top
            , Parser.map (\str li -> str :: li) (Parser.string </> restHelpO (maxDepth - 1))
            ]

Obviously the mutual recursion isn’t functionally necessary here – we just have essentially two copies of the original restHelp function calling each other – but apparently the 0.19.0 compiler treats this differently in a way that allows it to handle the generalized types as you’d expect. I think this also demonstrates that the original code @kradalby posted “should” have worked.

I’m not sufficiently confident to call this a compiler bug, but it does seem to have some similarity to this open issue, an example that compiled with 0.18 but not with 0.19.0 and that also involves recursion: