[web-devel] Type-safe URL handling

Jeremy Shaw jeremy at n-heptane.com
Fri Mar 26 12:30:46 EDT 2010


On Fri, Mar 26, 2010 at 10:30 AM, Michael Snoyman <michael at snoyman.com>wrote:

> I also am beginning to share a mistrust of classes; I think I went a little
> too overboard on them on a few previous packages (namely, convertible-text)
> and am now having a reaction in the opposite direction. I'm sure one day
> I'll find the Golden Path...
>

Sometimes I think that module functors  or adga's module system might be
part of the solution. But maybe only because I have not used those systems
much ;)


>
>
>> * I'd like to minimize dependencies as much as possible for the basic
>>> package. The two dependencies I've noticed are Consumer and
>>> applicative-extras. I think the type signatures would be clearer *without*
>>> those packages included, eg:
>>>
>>>    fromPathSegments :: [String] -> Either ErrMsg a
>>>
>>
>> Except that is not a usable type. fromPathSegments may consume, some, but
>> not all of the path segments. Consider the type:
>>
>> data SiteURL = Foo Int Int
>>
>> fromPathSegments is going to receive the path segments:
>>
>> ["Foo","1","2"]
>>
>> If you wrote a parser by hand, you would want it to look a little
>> something like:
>>
>>  do string "Foo"
>>       slash
>>       i <- fromPathSegments
>>       slash
>>       j <- fromPathSegments
>>      eol
>>      return (Foo i j)
>>
>> The key concept here is that when you call fromPathSegments to get the
>> first argument of Foo you need to know how many of the path segments were
>> consumed / are remaining, so you can pass only those segments to the second
>> fromPathSegments.
>>
>> So you really need a type like:
>>
>>    fromPathSegments :: [String] -> (Either ErrMsg a, [String])
>>
>> which outputs the unconsumed path segments.
>>
>> Well, given that as a criterion, I agree with the rest of your analysis
> entirely. However, I think we're looking at the purpose of fromPathSegments
> very differently. I'm not quite certain I understand why we would want to
> output the unconsumed segments; if something is unconsumed, then it seems
> like it's an invalid URL and should fail.
>
> In your example, if I request "/Foo/5/6/7", fromPathSegments would return
> (Right (Foo 5 6), ["7"]); but what is going to consume that 7 now? The use
> case I envisioned for something like this is:
>
> data BlogRoutes = ...
> data MySite = MyHome | MyBlog BlogRoutes
> fromPathSegments ("blog":rest) = MyBlog `fmap` fromPathSegments
>
>
But what if you had,

data BlogRoutes = ...
data Foo = ...
data MySite = MyHome | MyBlog Foo BlogRoutes

Where the MyBlog constructor has *two* arguments. In theory you want to
write something like:

fromPathSegments ("MyBlog":rest) = MyBlog `fmap` fromPathSegments ?? `ap`
fromPathSegments ???

The first fromPathSegments will parse the 'Foo' argument and the second
fromPathSegments will parse the BlogRoutes argument. To make things more
interesting, let's assume that Foo and BlogRoutes were defined in 3rd party
modules that don't even know about each other your app.

The problem is, what arguments do you pass to each fromPathSegments call?
The first call to fromPathSegments is going to consume some of the path
segments, and the second call will consume the remaining. But we do not have
enough information here to know in advance how to split up 'rest' between
the two calls. Instead we to run the first fromPathSegments and have it tell
us what part it did not consume.

If what I have said still does not make sense, then try this exercise:

create 3 modules, one each for:

data BlogRoutes = BlogHome
data Foo = Foo Int | Bar Char Int
data MySite =  MyBlog Foo BlogRoutes

now create fromPathSegments instances for each of those routes. I think you
will find that it is difficult to implement the instance for MySite.
Finally, can you now change the Foo type to:

data Foo = Foo Int Int | Bar Int Char Int

by *only* modifying the Foo module, and without breaking the MySite module?

Regarding the type:

data Foo = Foo Int Int

attempting to parse:

"/Foo/5/6/7"

I think that should be handled in, fromPathInfo :: (PathInfo u) => String ->
Failing u, when it calls fromPathSegments.

At that point in time we know that all the segments should have been
consumed... so if there is left over junk, something is wrong.

The latest version of the code is now at:

http://src.seereason.com/web-routes/

I did the renaming but have not made all the other changes yet.

- jeremy
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://www.haskell.org/pipermail/web-devel/attachments/20100326/cf0b0b46/attachment.html


More information about the web-devel mailing list