Mar 25, 2013

The same app 4 times: PHP vs Python vs Ruby vs Clojure

Here's a toy program I wrote implemented in PHP, Python, Ruby, and Clojure. I hope it's helpful for someone who knows at least one of those and wants to learn another.

The program is called “Nurblizer”, and it does one thing: Accept free-form text, and attempt to replace all words but the nouns in said text with the word “nurble”. It's up and running at http://nurblizer.herokuapp.com

First, the PHP. The repository can be found at https://github.com/adambard/nurblizer-php

I'm not a big fan of PHP, but you can't argue with the simplicity of deployment. The repository contains only 6 files!

The index.php file contains just HTML, because it doesn't actually have to do anything.

The magic happens in nurble.php

What happens here?

Ok, that seems to work (full disclosure: I didn't want to install Apache+PHP on my dev machine, so I haven't tested this one). Deployment in PHP works like this:

So why not stop there? Well, there are a few objections to be made to the PHP code above.

  1. The nouns.txt file is necessarily read and split on every request, which is not optimal
  2. The language is inconsistent in places. It's str_replace but strtoupper?

So next, we want to try Ruby. Unlike PHP, you can't just dump Ruby code on your server and expect it to work; you'll probably want to use a framework to do most of the lifting for you.

Enter Sinatra. Sinatra is a Ruby microframework which does a really great job of keeping things simple. You can find the nurblizer example here: https://github.com/adambard/Nurblizer

Like its big brother Rails, Sinatra doesn't mind making some assumptions on our behalf. It assumes we want our templates in views/ and our static files in public/

Enough about that. Here's the code:

So what's different?

The bit about dependencies is most interesting to me. In PHP, the problem of dependencies is offloaded to Apache, and by extension the server host. PEAR helps a lot, but even that's not guaranteed to be available everywhere. In our Ruby project, all our dependencies are by convention available for review and installable via rubygems.

Suppose now that we want to move to Python. Maybe we have performance contstraints now, and suspect Python would be faster (I'm told this is no longer true, as of Ruby 2.0, so insert your preferred pretext here).

In Python, the leading microframework is Flask, which deserves the spot as far as I'm concerned. There are a whole lot of others, but those don't have example nurblizer implementations built on top of them. Here's the one we'll be using: https://github.com/adambard/py-nurblizer

Flask puts templates in templates/ and static files in static/. Other than that, it's just like Sinatra (but in Python).

Note that Flask's approach is less like a DSL for web apps than Sinatra is. Instead of get and post blocks, we have routing decorators on regular old python functions. Note also that in Flask, we define an app object, add routes to it, and then explicitly run it at the end of the file.

Ok, so now our nurblizer application is getting really popular, and we need to port it to Clojure. Why? Because I already wrote the example, dammit. Here it is: https://github.com/adambard/nurblizer-clj

Clojure is overkill for this application, but it has some nice properties. It treats all data as immutable unless you go out of your way to make it otherwise, which is great for parallelism. It runs on the JVM, which is widely available, and it's pretty fast, too.

Writing web apps in Clojure is a bit different than using a microframework in another language. Noir is no longer maintained, but it's not too much more verbose to simply cobble together your own microframework from the components. So let's go do that!

The first thing you'll probably notice is that there are a lot more imports. We're using everything in compojure.core and nurblizer.helpers (the source of which is below). We use Ring and its Jetty adapter to run the server, Compojure to handle routing and plugging into ring. We also use Clostache, a Mustache implementation, to render the templates; that's the render function at the end of each view. We use Leiningen as a project manager/dependency resolver.

The nurblizer function is a lot more imposing in this one. We don't want to change the text in-place as we did with the other 3, because of that immutability thing mentioned before. Of course, we could always fall back on using Java objects and methods, but then we'd just be writing Java. So instead, we recursively update the text a word at a time.

Here's the render function:

So why Clojure? Well, for starters, its performance should be the highest of the above. This is doubly true if you use a server like httpkit, which I've tested up to 100 concurrent connections on a single heroku instance. You can also pack it up using lein uberjar and have a single file that you can run anywhere that has Java.

Does that make it worthwhile for this silly application? In my opinion, of course not. The Ruby version is easier to read and plenty powerful. But using Clojure from the get-go may save problems in the case that your project needs to be expanded in the future. It's also a lot of fun, but maybe that's just me.

Edit: This has generated some quality discussion on Hacker News, so check that out: https://news.ycombinator.com/item?id=5440170

About the nurblizer algorithm: several improvements have been suggested, the most obvious of which is to use a set or hash table to store the nouns. I encourage anyone to peer further into the pull requests on my assorted nurblizer repositories.

Also, some people have contributed their implementations in different languages:

Go

Javascript (Node.js)

JSP

Also, greyrest on Hacker News made this Clojure version, which is my favorite so far (and preserves whitespace to boot). I post it here because I don't think my efforts to mirror the other implementations in the Clojure version represent the language as well as I think it deserves.

    (ns nurblizer.core
      (:gen-class)
      (:use compojure.core)
      (:require
        [clojure.string :as str]
        [clostache.render :as clostache]
        [ring.adapter.jetty :only run-jetty]
        [compojure.handler :only site]))

    ;; main nurble stuff
    (def nouns
      (->> (-> (slurp (clojure.java.io/resource "nouns.txt")) ; read in nouns.txt
               (str/split #"\n"))                             ; split by line
           (map (comp str/trim str/upper-case))               ; feed the lines through upper-case and trim
           set))                                              ; transform into a set



    (def nurble-replacement-text "<span class=\"nurble\">nurble</span>")

    (defn nurble-word [word]
      (get nouns (str/upper-case word) nurble-replacement-text)) ; return word if word in set else nurble

    (defn nurble [text]
      (str/replace text #"\n|\w+" #(case %               ; using anon func literal, switch on argument
                                      "\n" "<br>"        ; when arg is newline replace with br
                                      (nurble-word %)))) ; otherwise nurble the argument (a word)

    ;; webserver stuff
    (defn read-template [template-file]
      (slurp (clojure.java.io/resource (str "templates/" template-file ".mustache"))))

    (defn render
      ([template-file params]
       (clostache/render (read-template template-file) params
                         {:_header (read-template "_header")
                          :_footer (read-template "_footer") })))

    (defroutes main-routes
      (GET "/"        []     (render "index" {}))
      (POST "/nurble" [text] (render "nurble" {:text (nurble text)})))

    (defn -main []
      (run-jetty (site main-routes) {:port 9000}))

Hey! Want to support posts like this and learn you some web development at the same time? Try Treehouse, a learning platform from the veritable geniuses at Carsonified.

Further Reading