I'm a full-stack developer who ends up doing more front-end work than most. I've used Rails for several years and Play for 16 months. Here's a comparison of Rails 3.1 with Play 2.1: hopefully the feedback can help improve Play.
TL;DR:
Play is less mature than Rails. Compared to Rails, it has strengths and weaknesses. For most future projects, I will be using Rails, because today Rails lets me build more features with less hassle.
A feature-by-feature comparison:
* Deployment
Play _should_ win here: jars are great, and the docs give lots of deployment options.
But Play uses Rhino to compile RequireJS files, so the compilation step is mercilessly slow. (This would be different on a website that doesn't use much client-side code.)
In my experience, Rails is a huge hassle when setting up a server environment; Play is more hassle during each deploy (only because deployment is so slow). As a result, Play deployment turns into an actual _thing_. With Play, I'll ask myself, "do I really need to deploy this important bugfix right now, or can it wait a week?" while on Rails, I see "git push && cap staging deploy" as a sensible shell alias.
Play could improve by making RequireJS compilation fast (probably by ditching Rhino).
Winner: Rails
* SBT
This deserves its own category. There's a whole level of hurt in Play that simply doesn't come about in Rails.
Both Rails and Play have satisfactory workflows for adding dependencies. After dependencies, though....
In theory Rails uses rake, but one rarely edits Rakefiles or even _runs_ rake in practice.
In Rails, tweaking the build involves editing a gem's configuration (in a separate file) or, with crappy add-ons, monkey-patching a gem (in a separate file). Key gems' configuration DSLs are clearly documented and _don't_ make me memorize quasi-random symbols like ":=", "++=", "<++=", .... And my Rails test suite doesn't crash with PermGen errors.
SBT is far too complicated and it crashes with PermGen errors when I keep "play ~test" open all day. That's more a critique of the Scala ecosystem than of Play in particular, but it hurts my efficiency when I develop with Play.
Winner: Rails
* Compiled vs Interpreted code (alternatively: "Scala vs Ruby")
I prefer compiled code in _theory_, but in _practice_ interpreted code lets me work faster. In Play it takes several seconds to run unit tests after any code changes, and it takes several seconds to refresh a page after I tweak a view.
A typical day of TDD might involve a bunch of cycles of this (using "play ~test-only [Spec]"):
1. [1 minute average] write or alter some test code (often five minutes, but more often five seconds, as I fix typos)
2. [15 seconds average] wait to see test result
3. [1 minute average] write or alter the code under test (often five minutes, but more often five seconds, as I fix typos)
4. [15 seconds average] wait to see test result
Result: when I'm coding at full efficiency with Play, I'm waiting 20 per cent of the time. On my Rails (or NodeJS) projects, I'm only waiting one or two per cent of the time.
This is frustrating. It makes me less inclined to write tests, which in turn makes my code less maintainable. Is this for a few milliseconds' speedup in production? That's premature optimization.
With that in mind, which programming language should one choose for a web framework? The main distinctions:
1. Scala+Play and Ruby+Rails take similar quantities of similarly-elegant code.
2. Rails is faster to test (and therefore develop)
3. Play is faster to run in production
... 2 matters every day; 3 only matters during optimization, at which point Rails has solutions.
Winner: Rails
* Asset management (JavaScript/CSS/Images/etc)
I'm a front-end developer. My day-to-day interactions with Play are fraught with frustration:
- It takes two minutes to tweak _one byte_ on a decent-sized, Bootstrap-based stylesheet suite on my netbook. _Two minutes_. On Rails, the same tweak would take _one second_.
- On Play, I can't avoid Google's Closure Compiler. I use CoffeeScript, so I don't need lint checking, yet it wastes a second or two every page refresh.
- Rails uses asset fingerprinting (e.g., "styles-908e25f4bf641868d8683022a5b62f54.css") and Play doesn't. To my knowledge, fingerprinting is the industry standard: it makes caching and cache invalidation seamless. Web pages load faster, CDNs are easy to update, and server load is reduced.
http://guides.rubyonrails.org/asset_pipeline.html
- Rails fingerprints images, PDFs and other assets, too -- not just CSS/JS.
- Rails lets you reference image routes from stylesheets and JavaScript; it lets you reference stylesheet routes from JavaScript, etc.
- Rails comes with Sass by default instead of Less. That's a marginal win for Rails, since Sass has some nifty features the Less developers deemed undesirable. (Loops, for instance, are a pain in the neck with Less.)
My top suggestion: Play should ditch Rhino for something faster. Rhino is way, way, way too slow. Rhino is unusable in development. Rhino is the biggest reason I fear Play.
After that, Play still has a ways to go to catch up with Rails. The docs at
http://guides.rubyonrails.org/asset_pipeline.html describe exactly what I'd expect a mature framework to support. (Lest you think Rails has tainted my judgement, consider that other asset managers have the same features: Yeoman and Jammit spring to mind, and there are mature, documented Grunt plugins for each of the features I've listed.)
I have implemented hacks:
Winner: Rails
* Asynchronicity
Play makes asynchronous code understandable. Rails either tries to hide it and sometimes doesn't support it.
I really appreciate Play's clarity and simplicity, as asynchronous code is important at a certain stage of development. It's simpler and more efficient than threading or forking.
Play's Iteratees are powerful and confusing; I admire Play for keeping them at the perfect level of abstraction so I don't need to know anything about them when I'm starting out.
Winner: Play
* Models
Here, Play has a huge opportunity.
Rails is opinionated: ActiveRecord or bust.
Play is un-opinionated. Unfortunately, Java's and Scala's database-y libraries are. We tried Ebean and quickly outgrew its feature set. We tried anorm and needed to use string concatenation to construct dynamic SQL strings. We switched to Squeryl and spent ages wrestling with its type-safe DSL.
Most Scala libraries we've used separate data objects from storage/retrieval methods. That's great ... for people who want to switch to another storage/retrieval library. See the irony? ;)
I haven't tried Slick; while it might be the great soon, I don't think it's ready for us today. It looks like there's some boiler-plate code involved, which means we'd need boiler-plate tests. Ick.
While ActiveRecord is a pain, its query interface -- arel <
https://github.com/rails/arel> -- is a dream. And its _default_ models are very simple, too: they require practically zero code. It's acceptable in practice.
I think Play should push ahead with Slick, in an opinionated manner: make it the default, iron out the wrinkly bits, and make the default use case as near to a one-liner as possible.
Winner: Rails (for today ... but not for long?)
* SQL Migrations/Evolutions
Rails migrations seem sensible: mostly code, plus SQL when you need it. You decide when to apply migrations: you can run them from the command-line with Rake.
Play evolutions are straight SQL, which makes for lots of boilerplate code.
Worse: there seems to be no way to test !downs.
Even worse: with Play, you can't deploy your evolutions with a single command. This leads to contortions on production servers during deployment.
And worse still: with Play, you can't *edit* evolutions without causing devastation on production. (1. Add a new feature with an accompanying evolution that adds a new table; 2. Deploy the feature; 3. Fix a typo in a comment in the evolution. 4. Deploy at a later date. You'll lose all data from the table on production.)
Play should add helpers akin to Rails', determine how to test evolutions, and provide a way to apply evolutions separately from server code.
Winner: Rails
* Forms
Rails has only one thing going for it when it comes to forms: it's CSRF-secure by default, because encourages and documents form helpers exclusively (as opposed to form HTML). This also applies to its AJAX helpers. (Rails comes with some JavaScript code to send CSRF-secure AJAX requests.)
But Rails' zest for permissively parsing form data has led to numerous security vulnerabilities, and in practice even the non-vulnerabilities can cause server errors. Play, on the other hand, has wonderful Form objects that convert from POST/GET variables to models and back. These objects are easy to test, and they completely avoid the problems Rails has with invalid input. All this greatness for a few lines of code -- it's totally worth it.
Play's only downside is that CSRF protection isn't the default. The common case is documented, at least.
Play could improve by documenting how to send CSRF-protected AJAX requests, and maybe adding a some HTML/JS helpers. (My solution is similar to Rails': a global JavaScript variable.)
Winner: Play
* Sessions (cookies, flash, etc.)
Play and Rails behave near-identically, so there's no winner here.
* Testing
Clear winner: Rails+RSpec. For every single line of test code.
- specs2 has nice matchers, but its DSL causes problems. For instance, if I import Squeryl, then I get two "in" methods; then, when there's a compilation error, Scala complains about "in" having two meanings, rather than complaining about the test in question.
- Scala tests take a while to compile. (It seems ironic to me that specs2 is optimized around concurrency. It _should_ be optimized around the real time-waster: compilation speed.)
- Play's documentation suggests dependency injection (without calling it so, nit), but the documentation is incomplete: it doesn't offer any suggestions for how to test AnormUserRepository.
- The suggestion for unit testing controllers is a good one, and it should be fleshed out. Tests like the one listed are crucial in practice, but the documentation doesn't explain _why_ this is so: why does the "should be valid" test use TestController instead of ExampleController? (I know the reason, but I can't see how a newcomer would learn that reason by reading that page.)
- FluentLenium is frustrating, because its return values are frequently null. If the tests pass, great! If the tests fail, they're hard to debug because one doesn't expect nulls in Scala. I need to remember to type Option() everywhere I call FluentLenium code ... which gets old.
- It would be handy to include FluentLenium (or a more Scala-ish library that has yet to be created) by default when testing templates.
- The documentation suggests using an H2 database. That is _usually_ a terrible idea, because H2 is so different from [database X] and those differences _usually_ matter. Play should nix this H2 suggestion entirely (it will only work for a minority of users, none of them novices), and instead document how to configure Play to use a test database for tests.
- Play has no documentation about setting up a database for tests. This is a Hard Problem that Rails has solved. In unit tests, Rails runs BEGIN TRANSACTION+ROLLBACK for all tests. In integration tests, things are more complicated.
- Play's WithApplication calls are frustrating. In RSpec, the "describe" blocks (equivalent to Play's "should" blocks) hold setup/teardown/around blocks, _not_ the individual tests. In my experience, that makes it easier to write tests.
- RSpec allows nesting "describe" blocks. That, plus per-block setup/teardown/around, means it doesn't need Specs2's icky "in new WithApplication" clauses. In practice, I find those clauses to be a subtle source of error: they're not where I _expect_ erroneous code to appear, because in Scala the key logic usually goes on the _left_ side of a line of code. I tend to try lots of other things before noticing I've made an error over there.
- RSpec makes mocking and dependency injection a bit easier, because Ruby objects are so malleable. Specs2 has Mockito, which is a start... but Mockito defaults to null return values, which are confusing in Scala.
- Play is missing documentation: How do I test Forms? BodyParsers? Filters? Etc.
- Why is testing on its own page in Play's documentation? I write my tests before I write my code, so the "how to test templates" code should come before the "how to write templates" code, and the "how to test controllers" code should come before the "how to write controllers" code.... (In other words: even though not everybody uses TDD or BDD, the Play framework should support and encourage it.)
My opinion: Specs2 is more complicated than RSpec, and that makes it more difficult (and hence more time-consuming) to write tests. There are things Specs2 does that RSpec doesn't, and there are things Specs2 does better than RSpec ... but those aren't as important as the things RSpec does that Specs2 doesn't, or the things RSpec does better than Specs2. Play would be easier (and faster) to use if it had a super-simple test framework with Specs2's matchers, RSpec's "describe" blocks, Mockito, and lots of opinionated helpers for testing various kinds of classes.
As for integration testing: were I coding my project from scratch, I'd write my integration tests in RSpec+Capybara (or RSpec+Cucumber+Capybara). I'm still tempted to do so, even for Play projects. The test suite is better. (I've often pondered whether it's possible, with JRuby, to port _all_ unit tests over to RSpec.)
Winner: Rails+RSpec
* Internationalization
The feature sets are identical and both implementations are usable and documented. Rails has a couple of pros:
- Rails has more documentation
- Rails uses a Yaml message file, which enforces namespacing
On the other hand, I can see how others would prefer Play because it uses Java MessageFormat strings in the standard MessageFormat files. Those strings are cryptic and the format is badly documented, but they _are_ standardized: maybe there are good tools for editing them?
In my particular project, I took advantage of the Messages format to translate some messages in JavaScript. See my JavaScript i18n function:
Winner: Both
* Routing
A typical Play project's routes file is more verbose than a typical Rails project's, because Rails has a standardized notion of a "resource". Rails also provides a convention for action names.
On the other hand, I like the way Play extracts paramaters. In Play, required query parameters are handled perfectly; in Rails, controller code needs to check their types (and if it doesn't, it can lead to security vulnerabilities).
Winner: Both
* Authorization/Authentication
On Rails I use Devise, a template for an industry-standard login setup. Devise includes HTML templates, database models and so forth.
With Play, nothing compares.
It took weeks, as I had to research best practices for password-reset protocols, registration email phrasing, and so forth. And my implementation doesn't use tokens, so it's not as secure as what I would have had on day one with Rails.
I'm aware of <
https://github.com/t2v/play2-auth>, and we started off with it before writing our own. It doesn't do all the things Devise does: for instance, sending registration emails. Those things are key, and it takes a lot of research to figure them all out.
Winner: Rails
* JSON
Play needs more code than Rails to accomplish the same thing. For instance, in Rails you can just call ".to_json" on any object and it'll do exactly what you'd expect.
As for parsing: I think Play does the best it can, considering it's type-safe. But Rails _isn't_ type-safe, so it takes less code to express the same thing.
Winner: Rails
MY TOP FIVE RECOMMENDATIONS FOR PLAY
I'm often a front-end developer, so my views are skewed....
If I were king of the world, I'd have Play:
1. Ditch Rhino and get a fast JavaScript engine for asset compilation. I can't see myself using Play for any new project until this happens.
2. Encourage developers to write tests first and code second. That means changes to documentation, helper classes, database integration, and perhaps a different DSL than specs2's.
3. Make Slick great and make Play opinionated about using Slick. Do what ActiveRecord does right: it makes the easy stuff easy and the hard stuff manageable. (To me, Play has no risk of doing what ActiveRecord does _wrong_, so Play can really shine here.)
4. Refocus asset management: there are industry standards for developing, bundling and serving JavaScript, CSS and image assets, and Play should follow them. Rails (Sprockets), Jammit and Yeoman showcase the standard features.
5. Work really hard making tests and code compile more quickly. At the very least, write documentation that helps developers write faster-to-compile code. For instance, can Scala code compile appreciably faster if I declare the types of my variables or if I (somehow) disable implicit variables? Or better yet, is there a way to run the tests with an interpreter rather than a compiler? Or to only recompile a single test at a time? Would it be productive to write core code in (gasp) Java for a compile-time win?
TAKE-AWAY THOUGHTS
I realize, after writing this, that Rails wins out an awful lot. I think that's mostly because it's a more mature framework.
In my 16 months with Play, I probably spent more than 40 per cent of my time doing things that I would not have needed to do with Rails. Put another way, I feel I was 40 per cent less productive than I should have been.
Many Rails wins are simply because Rails has better plugins. Rails doesn't ship with RSpec or Devise, for instance, but those plugins are part of the Rails universe. With Play I needed to code re-implementations or workarounds. I hope the necessary libraries come to Play soon.
Rails has a far, far faster development cycle. A faster "A. write tests; B. write code" cycle. Faster bundling. Templates and gems for testing all sorts of typical classes. Faster mocking (because classes are malleable). Less code (more conventions). Sure, a Play project responds to requests more quickly in production ... but that's rarely more important than development speed. (Indeed, with a faster development cycle it's faster to optimize the requests that matter.)
Play is a great tool for a wide range of tasks (streaming-heavy websites, for instance). In particular, its asynchronous nature is more intuitive than NodeJS's and more powerful than Rails'. I'm convinced Play will continue to improve at the speedy pace it has shown up to now.
Through Play (and Scala) I've gained an appreciation of static typing and immutable objects that will improve my future code.
So long, Play. It's been interesting.
Enjoy life,
Adam