A URL shortener made literate

This is a quasi-literate version of the very simple URL shortener I wrote in Haskell with Scotty.

This is a literate respin of an older post of mine

The original code listing is here


The code

First we declare our module name to be Main as that is required for anything exporting a main function to be invoked when the executable runs.

Next we have a series of imports. Where we import something qualified...as, we are doing two things: qualifying the import means that we can only refer to values in the module with the full module path. One example of that would be Control.Monad.mapM, that’s a fully qualified reference to mapM. Second, we use as to give the values that we want in scope a name. We qualify the import so that we don’t import values that would conflict with values that already exist in Prelude. By specifying a name using as, we can give the value a shorter, more convenient name. Where we import the module name followed by parentheses, such as with replicateM or liftIO, we are saying we only want to import the functions or values of that name and nothing else. In the case of import Web.Scotty, we are importing everything Web.Scotty exports. An unqualified and unspecific import should be avoided except in cases where it will be very obvious where a function came from or because it’s a toolkit you must use all together like Scotty.

Next we need to generate our shortened URLs that will refer to the links people post to the service. alphaNum is a String of the characters we want to select from - an alphabet if you will.

randomElement lets us get a random element from a non-empty list. It first gets the length of the list to determine what range it is selecting from, then gets a random number in that range using IO to handle the randomness. Nota bene - I could have written randomElement to return IO (Maybe a) instead of demanding a NonEmpty; however, I prefer to be more clear about my inputs if that cleans up my outputs. Being more clear about my inputs also provides the user more information than “maybe I won’t give you anything” does.

Here we apply randomElement to alphaNum to get a single random letter or number from our alphabet. Then we use replicateM 7 to repeat this action 7 times, giving a list of 7 random letters or numbers. For additional fun, see what replicateM 2 [1, 3] does and see if you can figure out why.

With saveURI, we pass our connection to Redis, the key we are setting in Redis, and the value we are setting the key to as arguments. We also perform side effects in IO to get Either R.Reply R.Status. The key in this case is the randomly generated URI we created and the value is the URL the user wants the shortener to provide at that address. Redis is a key-value datastore which can be very convenient for some common use-cases like caching…or when you want persistence without a lot of ceremony, as was the case here.

In the case of getURI, we just pass it the connection to Redis and the shortened URI key so that we can get the URI associated with that short URL and show the users where they’re headed.

main is a function like any other. Here it returns IO () and acts as the entry point for our webserver when we start the executable. We begin by invoking scotty 3000, a helper function from Scotty which, given a port to run on and a Scotty application, will listen for requests and respond to them.

Outside of any specific route handlers, we are cheating a bit and binding the database connection to Redis to the name rConn. R.defaultConnectInfo points at an instance of Redis on my local machine, and liftIO lifts the IO action into the Scotty monad. liftIO is a typeclass method from MonadIO; monads which implement it can have IO actions lifted over them. Writing a trivial instance of this can be a good way to get a feel for how this works and why it’s desirable. Briefly - it saves us a bunch of lift . lift . lift etc.

get has type RoutePattern -> ActionM () -> ScottyM (). It takes a RoutePattern to decide which request paths to dispatch on and an action to perform when a request matches that path. It (purely) modifies the enclosing ScottyM to register itself.

Here we use param to get the input parameter “uri”. Where param gets its parameters depends on the HTTP method of the action. With a get it will check the HTTP get arguments, whereas with a post it will check the POST form body. In all cases, it will check for parameters in the URI itself, such as with “/my/uri/:argument”.

Since we want to reject things that aren’t URIs or might’ve been accidentally malformed, we’re using parseURI which has type String -> Maybe Network.URI.URI to discriminate between bad and good URIs. We are using TL.unpack because param returned a lazy Text object and not the String parseURI expects, so we unpack it into a String. Then we case match on the Just and Nothing case of the Maybe parseURI returns to handle the “yep, a proper URL” and “nope, not a proper URL” cases separately.

Here we match on the Just constructor of Maybe and throw away the contents because we only used parseURI to check if the URI was correct; we don’t actually care to use the parsed URI object.

Now we call on our old friend shortyGen to get a randomized URI to assign a shortened URI for the input URL the user gave us. Once again, shortyGen returns IO so we must lift that over our ActionM. Previously, we were lifting over ScottyM. Both, conveniently, implement MonadIO. One of the nice things about liftIO is that we don’t have to care how many monads the IO action got lifted over. This can save some maintenance and headache down the road.

We use a let expression with let shorty = BC.pack shawty because it’s not participating in the enclosing monad. It just packs the String into a ByteString.

Here we’re encoding the uri the user passed us as a ByteString as that’s what the Redis client library wants. Before it can be encoded, we have to change it from a lazy to a strict Text object. We’re using the Redis connection in scope from earlier and the shortened URI ByteString to save the user’s data at that key in the Redis database. We lift the IO action into the Action monad transformer and bind over the result in ActionT. The response has type (after monadic binding) Either R.Reply R.Status.

We stringify resp which is Either R.Reply R.Status. It’ll be Right Ok ...yadda yadda if it succeeded, Left ... if it failed. We concatenate the stringified response with some additional text to show the user what the shortened URI is.

Now we need to handle the Nothing case from earlier. The Nothing case happens if parseURI was given something that wasn’t a correct URL. We just return a text response saying the uri provided wasn’t a url.

This is another http get request handler, but this time with a parameterized URL component which has the name short. This is so a request against /EFG3YLB will parse EFG3YLB as the param short.

As before, param does a bit of magic for us. param checks multiple sources of arguments for us. Where previously we were getting a parameter from the HTTP GET arguments, this time it’s a component of the request path.

Here we lift an IO action into ActionT. This time we’re taking the shortened URI path that we got and asking Redis if it has a matching value for us to give back to the user at that key. We bind over the result, giving us uri which has type Either R.Reply (Maybe BC.ByteString).

Next we case match on uri over the Left and Right constructors of the Either sum type. If we get Left…well, we’re done. We show the user what Redis had to say about it and give up. If we got Nothing that means Redis didn’t have a value saved at the key we queried - nothing stored at that location. In that case, we return the text to the user “uri not found”.

If we got Right, we’re going to proceed.

We proceed because the Right value means the Either value was a “success” idiomatically speaking. Here mbBS is the Maybe BC.ByteString that was inside of Either R.Reply (Maybe BC.ByteString).

If we got Just, we take the ByteString Redis returned and pack it into an HTML anchor tag so it is a clickable link for the user to follow. If that happened, the request was a success and we’re done. We use the html instead of text Scotty function to indicate the content type we’re returning is HTML and not plain text.

I know this site is a bit of a disaster zone, but if you like my writing or think you could learn something useful from me, please take a look at the Haskell book I've been writing. There's a free sample available too!