reddit-scala
文件大小: unknow
源码售价: 5 个金币 积分规则     积分充值
资源说明:Scala "clone" of Reddit
h1. About

Today I randomly stumbled over Lau Jensen's "blog article":http://www.bestinclass.dk/index.php/2010/02/reddit-clone-in-10-minutes-and-91-lines-of-clojure/ ("Part 2":http://www.bestinclass.dk/index.php/2010/02/reddit-clone-with-user-registration/) where he presents a "clone" of "Reddit":http://www.reddit.com/, implemented in a mere 91 lines of "Clojure":http://clojure.org/ 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.


h1. The original application

h2. Basics

Lau's Clojure program uses "Compojure":http://github.com/weavejester/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":http://github.com/alandipert/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)
        
  • {title} Posted {buf.toString} ago, {score} points Up Down
  • } }
    I used the same date/time formatter as Lau did - "Joda Time":http://joda-time.sourceforge.net/ -, 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. h2. 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]) = {
        
            
                {title}
            
            
                {content}
            
        
    }
    
    Scala allows both single XML elements and sequences of elements as literals; the former are turned into @scala.xml.Elem@s, the latter into @scala.xml.NodeBuffer@s. A @NodeBuffer@ is-a @Seq[Node]@, and as we see in the above code, those integrate into @Elem@s just fine. h2. The Home Page Using the above template method, I render my home page like this:
    
    get("/") {
        createHtml("Reddit.Scala",
            

    Reddit.Scala

    In slightly more than 100 lines of Scala

    Refresh Add link

    Highest ranking entries

      { renderLinks {(e1, e2) => e1.score > e2.score} }

    Most recent submissions

      { renderLinks {(e1, e2) => e1.date isAfter e2.date} }
    ) }
    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 @
      ...
    @ 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 @–@ separating the two links inside a @@. h2. Adding Links The form for submitting links looks, naturally, quite similar to Lau's:
    
    get("/new") {
        createHtml("Reddit.Scala: submit new link",
            

    Submit new link

    { params("msg") }
    URL:
    Title:
    ) }
    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 @@ 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. h2. 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. h2. Conclusion The "entire program":http://github.com/ulrichsg/reddit-scala/blob/master/src/reddit-clone.scala 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". h1. Extension: Adding user management The first thing Lau added to his original Clojure program was "user registration":http://www.bestinclass.dk/index.php/2010/02/reddit-clone-with-user-registration/, so naturally I was also going to do this. I didn't except it to be difficult, and indeed it was not. :-) h2. 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
    }
    
    h2. 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) =
        
            {label}
            
        
        
    def submitButton(caption: String) =
        
              
        
    
    def form(method: String, url: String, content: Seq[Node]) =
        
    {content}
    This allows me to specify my login screen in a very concise manner:
    
    get("/login") {
        createHtml("Reddit.Scala: log in",
            

    Log in

    { params("msg") }
    { form("post", "/login", List( textField("Username:", "username", 25, ""), textField("Password:", "password", 25, ""), submitButton("Enter"))) }
    ) }
    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. h2. 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("/")
    }
    
    h2. 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. h3. Disclaimer Reddit is (C) 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.

    本源码包内暂不包含可直接显示的源代码文件,请下载源码包。