Skip to content

ulrichsg/reddit-scala

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 

Repository files navigation

About

Today I randomly stumbled over Lau Jensen’s blog article (Part 2) where he presents a “clone” of Reddit, implemented in a mere 91 lines of Clojure to showcase the language’s highly compact syntax. Always eager to compare Clojure with other languages, Lau was asking for people to produce Scala and Haskell implementations in particular. Now, since I am very interested in Scala, but never had the opportunity for some practical work in it before, I spontaneously decided to take up his challenge. A few hours later (I’m a newbie, remember), and here is Reddit.Scala!

Note that I’m not shooting for the least possible number of lines with this program; rather, I’m aiming for readable and idiomatic Scala code – the kind of code that I like working with best.

The original application

Basics

Lau’s Clojure program uses Compojure, a lightweight web framework, for building HTML pages and processing HTTP requests. Now, Scala has XML literals built into its syntax, so I don’t need any framework to build pages, but I still have to deal with the request processing part. Fortunately, there is a Scala framework called Step that can do this for me. It’s actually quite similar to Compojure, aside from the fact that it does not have any factory methods for HTML elements.

I decided to model my data using a class. I could have done it like Lau and used hash maps, but in an object-oriented language like Scala, I think classes make the most sense. For a little extra conciseness, I made it a case class, declaring the score member mutable so I can adjust the score later without much hassle. I also gave it a method to output itself as an HTML element:


case class Entry(title: String, url: String, var score: Int, date: DateTime)
{
    def toHtml = {
        val printer = new PeriodFormatterBuilder()
            .appendDays.appendSuffix(" day ", " days ")
            .appendHours.appendSuffix(" hour ", " hours ")
            .appendMinutes.appendSuffix(" minute ", " minutes ")
            .printZeroAlways()
            .appendSeconds.appendSuffix(" second", " seconds")
            .toPrinter
        var buf = new StringBuffer
        printer.printTo(buf, new Period(date, new DateTime), Locale.getDefault)
        <li>
            <a href={url}>{title}</a>
            <span style="size: -1; color: gray;">Posted {buf.toString} ago, {score} points</span>
            <a href={"/up/"+url}>Up</a>
            <a href={"/down/"+url}>Down</a>
        </li>
    }
}

I used the same date/time formatter as Lau did – Joda Time -, thus our versions look quite similar once you get over the syntactic differences between Scala and Clojure. The extra printZeroAlways() makes sure that even a period of “0 seconds” is printed. Joda Time’s documentation suggests that this should be the case by default, but for me it wasn’t.

Templating With Step

In the Step framework, GET or POST requests to a particular URLs are mapped to handlers in a way somewhat different from Compojure’s. Here I call get("/my/path", handler) for every URL I want to accept, where handler is a function that returns an XML literal. Those functions are independent from the framework, so I can cut a bit of boilerplate by putting the HTML scaffold into a separate method:


def createHtml(title: String, content: Seq[Node]) = {
    <html>
        <head>
            <title>{title}</title>
        </head>
        <body>
            {content}
        </body>
    </html>
}

Scala allows both single XML elements and sequences of elements as literals; the former are turned into scala.xml.Elems, the latter into scala.xml.NodeBuffers. A NodeBuffer is-a Seq[Node], and as we see in the above code, those integrate into @Elem@s just fine.

The Home Page

Using the above template method, I render my home page like this:


get("/") {
    createHtml("Reddit.Scala",
        <h1>Reddit.Scala</h1>
        <h3>In slightly more than 100 lines of Scala</h3>
        <a href="/">Refresh</a>
        <span> &ndash; </span>
        <a href="/new">Add link</a>
        <h2>Highest ranking entries</h2>
        <ol>{ renderLinks {(e1, e2) => e1.score > e2.score} }</ol>
        <h2>Most recent submissions</h2>
        <ol>{ renderLinks {(e1, e2) => e1.date isAfter e2.date} }</ol>
    )
}

The only interesting part in this otherwise plain sequence of HTML elements are the calls to renderLinks. Here’s how this method looks like:


def renderLinks(sortFunc: (Entry, Entry) => Boolean) = 
   for (entry <- data.sort(sortFunc)) yield entry.toHtml

Amazingly concise. No surprise, though, this is functional programming after all.

By the way, I initially tried to place the <ol>...</ol> tag in the toHtml method, but Scala didn’t allow me to put code inside an XML element sequence at the top level. This is also why I had to wrap the &ndash; separating the two links inside a <span/>.

Adding Links

The form for submitting links looks, naturally, quite similar to Lau’s:


get("/new") {
    createHtml("Reddit.Scala: submit new link",
        <h1>Submit new link</h1>
        <span style="color:red">{ params("msg") }</span>
        <form action="/new" method="post">
            <div>URL: <input type="text" name="url" size="48" value="http://"/></div>
            <div>Title: <input type="text" name="title" size="48"/></div>
            <input type="submit" value="Add link"/>
        </form>
    )
}

One notable difference is that Compojure’s HTML factory automatically aligns the form elements nicely. With Step I would have to do this manually, So here’s a little optical advantage for the Clojure solution.

I also don’t check if msg is empty. As I mentioned before, due to Scala’s rules for XML sequences I have to use an element tag whenever I want to insert some data – I can’t choose to maybe insert something dependent on some boolean expression. However, if there is no message, the <span/> will be invisible anyway, so I just insert it without checking. Not the cleanest solution, but it suffices for my purposes.

Here’s the POST handler that processes my input:


post("/new") {
    val title = params("title").trim
    val url = params("url").trim
    val target =
        if (title isEmpty) "/new?msg=Invalid Title!"
        else if (invalidUrl(url)) "/new?msg=Invalid URL!"
        else if (data.exists{_.url.equalsIgnoreCase(url)})
            "/new?msg=Link already submitted!"
        else {
            data = Entry(title, url, 1, new DateTime) :: data
            "/"
        }
    redirect(target)
}

This is almost a direct translation of Lau’s method, with a couple of @val@s thrown in to hold the data I receive via params() (Compojure has already extracted the parameters in its defroutes function). I like how I can use if as an expression rather than a statement, giving some extra conciseness to the assignment to target. I suppose I could do away with target and plug the if expression directly into redirect(), but I prefer keeping the extra variable for clarity.

When it came to invalidUrl, I was very impressed with Lau’s use of a try...catch expression. Nifty! To my great pleasure, I realized Scala can do that, too:


def invalidUrl(url: String) =
    try { val foo = new java.net.URL(url); url isEmpty } catch { case _ => true }

I initially wrote def invalidUrl(url: String) = url isEmpty || try { ... }, but I kept getting errors, so I moved the isEmpty call to the inside of the try block. This means I always create a URL object even if url is empty, but that is hardly a critical waste of resources.

Rating Posts

Only two handlers left – up/ and down/. They do almost the same, so I moved the common code into a separate method:


def rate(url: String, value: Int) = {
    data.synchronized {
        data.find(_.url.equalsIgnoreCase(url)) match {
            case Some(entry) => entry.score += value
            case None => ()
        }
    }
}

get("/up/:url") {
    rate(params(":url"), 1)
    redirect("/")
}

get("/down/:url") {
    rate(params(":url"), -1)
    redirect("/")
}

Step can extract parameters directly from the request URL if you specify them with a leading colon in the argument’s handler. (This works even when there are forward slashes in the argument, as is common for URLs).The rate() method shows how Scala handles synchronization. Otherwise there is nothing special to see here.

Conclusion

The entire program spans 111 lines, i.e., 20 more than the Clojure solution. This isn’t surprising – Scala doesn’t quite reach the level of terseness that Clojure has (and I could certainly shave off a few more lines if I tried, but I don’t want to) – but I am very satisfied with it nonetheless. The code is much more compact than anything I could have managed with Java, and at the same time highly elegant and readable; in particular, someone unfamiliar with Lisp will probably find it much more readable than Clojure. I therefore contend that both languages are on equal footing in this “contest”.

Extension: Adding user management

The first thing Lau added to his original Clojure program was user registration, so naturally I was also going to do this. I didn’t except it to be difficult, and indeed it was not. :-)

Storing Data

As before with Entries, I use a case class to store user data. This time it’s even easier because I need neither mutable fields nor rendering methods. I keep registered users in a list and online users in a mutable HashSet, which looks very easy to use:


case class User(name: String, password: String, email: String)

var registeredUsers = List(User("UlrichSG", "password", "ulrichsg@somewhere.de"))

var onlineUsers = new HashSet[User] with SynchronizedSet[User]

The with SynchronizedSet[User] part is a mixin, which is a very cool feature of Scala. In this case it makes my HashSet threadsafe so I don’t have to explicitly synchronize over it later.

I also create a small utility method to easily access the record of the user curently logged in, if any. Scala’s Option class makes handling the “if any” part quite painless:


def currentUser = session("username") match {
    case Some(username) => registeredUsers.find(_.name == username)
    case None => None
}

Some More Templating

In the original version of my program, the HTML form looked somewhat disorderly because I didn’t want to blow up my code with table definitions. However, now I am going to need two more forms (for registering and logging in), so it would make sense to create custom building functions for them – and that means I can add formatting quite efficiently.

Basically, I recreated Compojure’s form-to function. Not having an HTML generator provided by the framework, I also added “sub-functions” for the input elements:


def textField(label: String, name: String, length: Int, default: String) =
    <tr>
        <td>{label}</td>
        <td><input type="text" name={name} length={length.toString} value={default}/></td>
    </tr>
    
def submitButton(caption: String) =
    <tr>
        <td colspan="2"> <input type="submit" value={caption}/> </td>
    </tr>

def form(method: String, url: String, content: Seq[Node]) =
    <form action={url} method={method}>
        <table cellspacing="5">
            {content}
        </table>
    </form>

This allows me to specify my login screen in a very concise manner:


get("/login") {
    createHtml("Reddit.Scala: log in",
        <h1>Log in</h1>
        <span style="color:red">{ params("msg") }</span>
        <div>{ form("post", "/login", List(
            textField("Username:", "username", 25, ""),
            textField("Password:", "password", 25, ""),
            submitButton("Enter"))) }</div>
    )
}

I am not perfectly happy with this solution yet. Firstly, if I could give the text field’s default parameter a default value of "", that parameter could be dropped from most of this function’s invocations. Unfortunately, the current Scala version does not suport default arguments. Fortunately, the upcoming Scala 2.8, which is currently in beta, will.

Additionally, password fields really should have type="password" instead of type="text". This, too, could be solved with a default argument.

Handling Logins and Registrations

HTML forms are not terribly interesting, so let’s look straight at the business code. Just like Compojure, Step supports session handling:


post("/login") {
    val username = params("username").trim
    val password = params("password").trim
    registeredUsers.find(_.name == username) match {
        case Some(user) if user.password == password => {
            session("username") = username
            onlineUsers += user
            redirect("/")
        }
        case _ => redirect("/login?msg=Unknown username or wrong password")
    }
}

I first tried to match the user as case Some(user: User(_, password, _)) => ..., but for some reason this did not compile. Using a guarded match works just fine, though, and is even more intuitive.

Logging out is a fairly trivial matter now:


get("/logout") {
    currentUser match {
        case Some(user) => onlineUsers -= user
        case _ => ()
    }
    session.invalidate
    redirect("/")
}

Conclusion

I’ll skip over the rest, as it doesn’t introduce any new concepts. All together, I have added 100 lines to the program, which is now 211 lines long – Lau’s Clojure version weighs in at 160. It would seem that Scala is falling behind faster than expected in terms of pure LOC, but I think the difference is due to the HTML form helpers that Lau did not have to write because Compojure already allows him to define his forms in a very concise notation. The code is now long enough to benefit from splitting into multiple files – the HTML helpers being a particularly good candidate -, but like Lau I’ll make an exception here. Anyway, Scala is still cheerfully and elegantly handling every task I use it for. The only thing I was missing were default argument values. However, these – as well as keyword arguments, another very nice feature – will be in the next release, so there’s no need to complain.

Disclaimer

Reddit is © 2010 Conde Nast Digital. This code has nothing to do with the actual reddit.com site and is not intended to violate their rights (nor is it actually capable of doing that). It is just a little exercise in programming.

About

Scala "clone" of Reddit

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages