Haskell / Snap ecosystem is as productive as Ruby/Rails.
This may be controversial, and all of the usual disclaimers apply - this is based on my own experience using both of the languages/frameworks to do real work on real projects. Your mileage may vary. Because this is something that has the potential to spiral into vague comparisons, I am going to try to compare points directly, based on things that I’ve experienced. I am not going to say “I like Haskell better” or anything like that, because the point of this is not so much to convince people about the various merits of the languages involved, just to point out that I’ve found that they both are as productive (or that Snap feels more so). For Haskell programmers, this could be an indication to try out the web tools that you have available, especially if you are usually a Rails developer.
As a note - some of this could also apply to other haskell web frameworks (in particular, most of this pertains to happstack, and some pertains to yesod), but since Snap is what I use, I want to keep it based on my own personal experience.
1.
The number one productivity improvement is a smart
strong type system. This is less of an issue for small projects, but as
soon as you have at least a few thousand lines of code, adding new
features or refactoring inevitably involves changes to multiple parts of
the codebase. Having a compiler that will tell you all the places that
you need to change things is an amazing productivity booster. This can
be approximated in some ways with good test coverage, but it is really a
different beast - tests often need to be changed as well, and if you
aren’t very careful about this it is easy to change them in ways that
don’t catch new bugs. Additionally, it is hard (or very tedious, if you
do it wrong) to achieve high enough coverage to actually catch all of
the bugs introduced in refactoring. This as compared to a compiler that
is completely automated and will always be aware of all of the code you
have and the ways that it interacts (at least to the extent that you
actually use the type system - but if you are a good haskell programmer,
you will).
This alone wouldn’t be enough to suggest using Haskell/Snap over Ruby/Rails, as a type system isn’t worth much without supporting libraries, but as I switch between the ecosystems, this is the place where I notice the most drastic improvements in productivity, so I put it first.
2.
Form libraries. There are many different libraries
for dealing with forms in Rails, and there is the built in one as well.
The general idea is that you define some validations on your models, and
then use the DSLs from the form libraries to define forms, and can do
validations, etc. In Haskell (in my opinion), the best form library is
Digestive-Functors (thanks Jasper!), and the productivity difference is
staggering in more complex use-cases. In the sort of vanilla examples
that rails has, the validation system works quite well, and dynamic
introspection allows you to write really short forms. This begins to
break down when you start getting forms that don’t correspond in a
simple way to models. I have forms that are sometimes a mix of two
models, or forms that are a partial view into a data structure, or any
number of other variations.
With Digestive-Functors, I can define the forms that I need, and re-use components between multiple forms (forms are composable), and these validations are on the form, not on the underlying model. It is obviously useful to database level data integrity checks, but I find that having them being the main / only way of doing validations is really limiting - because sometimes there are special cases when you want the validation done one way and other times another.
More generally, it is possible that the business logic of a specific form may have requirements that do not always have to hold for the datastore, and thus should not reside in the integrity checks. Having written a lot of forms (who hasn’t?), I find that getting the first form out is much faster with Rails, but inevitably when I need to change something it starts become difficult fast. Every time I am doing it I keep picturing an exponential curve - sure it starts out really small, but it gets really big really fast! It isn’t that I run into things that are not possible with Rails, but they end up being more difficult, more error prone, and generally reduce my productivity. With Digestive-Functors, I spend a little more time building the forms in the beginning, but I’ve never had requirements for a form that weren’t easily implemented (almost without thinking).
3.
Routing is the next big one. This may be more of an
opinion that the previous ones, but I have always thought that great
care should be involved in designing the url structure of a site. In
this sense, I guess I disagree with the idea of universally using REST -
I think it is very useful when writing APIs, but when designing
applications for people, I believe the urls should be meaningful to the
people, not to machines. Usually, right after modeling the data of an
application, I make a site-map - this is a high level view of what the
site should look like. Instead, with Rails, I spend time thinking of how
I can adapt what I want to the REST paradigm, and usually end up with
something that is an incomplete/counterintuitive representation.
More broadly, I think the idea of hierarchical routing is brilliant - the idea that you match routes by pieces. What this allows you to do is easily abstract out work that should be done for many different related requests. In Rails, this is approximated by :before_filters (ie, it a controller for a specific model, you might fetch the item from the id for many different handlers), but it is a poor substitute. For example I often have an “/admin” hierarchy, and to limit this, all I have to do is have one place (the adminRouter or something) that does the required work to ensure only administrators can access, and it can also fetch any data that is needed, and then it can pass back into the route parsing mode. Or if I want to do the rails-style pre-fetching, then I design the routes as “/item/id/action” and have a handler that matches “/item/id”, fetches the item, and then matches against the various actions. If I have nested pieces of data, this is just as easy. I could have “item/id/something/add” which adds a new “something” to the item with id “id”, This would all be in the same hierarchy, so the code to fetch the item would still only exist once.
Not only is this very natural to program, it keeps the flow easy to follow when you are looking back at it, and allows backtracking in a great way: if, in a handler, you reach something that indicates that this cannot be matched, like if the path was “/item/id” but the id did not correspond to an actual item, you can simply “pass” and the route parser continues looking for things that will handle the request. If it finds nothing, it gives a 404.
An example of how you could exploit things in a really clean way - if you are building a wiki-like site, then you first have a route that matches “/page/name” and looks up the page with name “name”. If it doesn’t find it, it passes, and the next handler can be the “new page” handler, that prompts the user to create the page. As with everything else, I’m not saying this cannot be done with Rails, simply that it is much more natural and easy to understand with Snap (and Happstack, where this routing system originated, at least in the Haskell world).
4.
Quality of external libraries. Point 2 was a special
case of this, since dealing with forms comes up so much, but I think the
general quality of libraries in Haskell is superb. One example that I
came up against was wanting to parse some semi-free-form CSV data into
dates and times. Haskell has the very mature parsing library Parsec
(which has ports into many languages, including Ruby) that makes it
really easy to write parsers. I ported an ad-hoc parser to it, and found
that not only was I able to write the code in a fraction of the time,
but it was a lot more robust and easy to understand.
For testing of algorithmic code, the QuickCheck library is pretty amazing - in it, you tell it how to construct domain data, and then certain invariants that should hold over function applications, and it will fuzz-test with random/pathological data. The first time you write some of these tests (and catch bugs!) you will wonder why you haven’t been testing like that before! I don’t really want to go into it here, but the other point is that many of these libraries are very very fast - there has been, over the last couple years, a massive push to have very performant libraries, with a lot of success. The Haskell web frameworks webservers regularly trounce most other webservers, and there are very high performant json, text processing, and parsing libraries (attoparsec is a version of parsec that is very fast).
5.
Templating. In this, I want to directly compare the
experience of using Heist (a templating system made by the Snap team)
and Erb/Haml (I mostly use the latter, but in some things, like with
javascript, I have to use the former). The first big difference is the
idea of layouts/templates/partials in rails. I never really understood
why there was this distinction when I first used it, and when comparing
it to Heist (which has no distinction - any template can be applied to
another, to achieve a layout like functionality, and any template can be
included within another, to achieve a partial like functionality) it
feels very limited.
The other major difference is that the two templating languages in
Ruby allow dynamic elements by embedding raw ruby code, whereas the
former allows dynamic stuff by allowing you to define new xml tags
(called splices) that you can then use in the templates. I have found
this to be an extremely powerful idea, as it allows you to not only do
all the regular stuff (insert values, iterate over lists of values and
spit out html), but can even allow you to build custom vocabularies of
elements that you want to use that are designed to go with javascript
(so for example, I built an asynchronous framework on top of this, where
I had a “<form-async>
” tag and
“<div-async>
”s that would be replaced asynchronously
by the responses from the form posts).
It also adapts to being used with (trusted) user generated input -
I’ve used it in multiple CMS systems so that, for example, all links to
external sites are set to open in new tabs/windows (by overriding the
“<a>
” tag and adding the appropriate “target”) or
allowing the users to gain certain dynamic stuff for their pages.
Compared to this, the situation with Haml always seems hopelessly tied
up with ruby spaghetti code - not that it always is (you can always be
careful), but the split with Heist both feels like a cleaner separation
AND more powerful, which is not something you get often, and I think is
a sign that the metaphor that Heist created (which is based on a couple
really simple primitives) is really something special.
6.
This is sort of an extension of the first point, and
I’m putting it towards the end because it is the most subjective of this
already quite subjective comparison - I think that web applications
built with Haskell/Snap are much easier to edit / add to than
corresponding applications in Ruby/Rails. One of the biggest reasons for
this is that there is much more boilerplate/code spread in ruby - some
of it is auto-generated, other bits is manually generated, but there
ends up being code scattered around. It is pretty easy to add new code,
but when you want to edit / refactor existing code, it starts to get
hard to figure out where everything is. A bit of this relies on
conventions to a degree (which you learn), but there is simply less code
in Snap, and usually everything pertaining to a specific function is in
one place. This has a lot to do with the functional paradigm - there is
no hidden state, so generally all the transformations that occur are
very transparent, whereas with Rails it is possible for stuff from the
ApplicationController being applied, or just various filters coming into
play, or stuff from the model, etc. There is no obvious “starting point”
if you want to see how a request travels through your application
(candidates include the routes file, the controllers, etc), in the same
way where with a Snap application, the code to start the web server is
in one of the files you write! You can trace exactly what it is doing
from there!
In addition, there is also very little “convention” with Snap. It enforces nothing, which has the consequence (in addition to allowing you to make a mess!) of having the whole application conforming to exactly how you think it should be organized. I’ve found that this actually makes it much easier to add new things or modify existing functionality (fix bugs!), because the entire structure of the application, from how the requests are routed to how responses are generated, is based on code I wrote. This means that making a change anywhere in this process is usually very easy - it feels in some ways like the difference in making a change to an application you wrote from scratch and one that you picked up from someone else. There is also a potential downside to this - the first couple applications I built had drastically different organizational systems
(Side note for anyone reading this who is curious: I’ve converged to the following method: all types for the application lives in a Types module or hierarchy, all code that pertains to the datastore lives in a State hierarchy or module in a small application, code for splices lives in a Splices hierarchy, forms live is a Forms hierarchy, and the web handlers live in a Handlers hierarchy. I also usually have a Utils module that collects some various things that are used in all sort of different places. Everything depends on Types and Utils. Splices, Forms, and State are all independent of one another, and Handlers depends on everything. And then of course there is an Application module and Main, according to the generated code from Snap).
This is a major difference in how Snap even differs from some other Haskell web frameworks, that it seems more like a library with which to build a web application instead of a true framework, but in my experience this is actually a really powerful thing, and makes the whole process a lot more enjoyable, because I never feel like I’m trying to conform to how someone else thinks I should organize things.
7.
I’m bundling the performance, security, etc all at
once. Rails is a very stable framework, so lots of work has gone into
this. But I think the recent vulnerabilities exposed on a lot of major
sites (like GitHub) based on the common paradigm of mass-assignment sort
of point out the negative side. Snap is much newer, but it was built
with security in mind from the beginning, as far as I can tell, and most
libraries that I have used have also mentioned ways that it comes up -
the entire development community seems a lot more aware / concerned with
it.
I think part of this probably has to do with the host languages - ruby is a very dynamic language that has a history of experimentation (so generally, flexibility is preferred of correctness), whereas Haskell is a language where lots of static guarantees are valued, and security is usually lumped in with correctness. For performance, there is no question that Haskell will win hands down on any performance comparison (and on multithreading). Granted, a lot of web code is disk/database bound so this isn’t a huge deal, but it is nice to know that you aren’t needlessly wasting cycles (and can afford to run on smaller servers).
8.
Now, as a counterpoint, I want to articulate what
Rails really has over Snap. Number one, and this is huge, is the size of
the community. There are a massive number of developers who know how to
use Rails (how many are good at it is another question), and this also
means that if you are trying to do something it is much more likely that
a prebuilt solution exists. It also means that it will be easier to hire
people to work on it, and easier to sell it as a platform to
clients/bosses.
The Haskell community is surprisingly productive given its size (and some of the tools it has produced are amazing - examples mentioned in this comparison are Parsec, QuickCheck, Digestive-Functors, etc), but there is some sense where they will always be at a disadvantage. This means that if you are doing any sort of common task with Rails, there will probably be a Gem that does it. The unfortunate part is that sometimes the Gem will be unmaintained, partially broken, incompatible, as the quality varies widely. This is a place where a lot of subjectivity comes in - I have found that most of what I need exists in the haskell ecosystem, and if stuff doesn’t it isn’t hard to write libraries, but this could be a big dealbreaker for some people.
Cheers, and happy web programming.