Many tools makers realized that this is not the way to go and started working on unifying some of those aspects in more comprehensive solutions for the Web (Google Web Toolkit is one result of such efforts). In a world where dynamic programming and Agile development are king, two modern incarnations of the comprehensive Web solution concept are Node.js and Opa. Both feature an event-driven model that makes them a good fit for writing “real-time” web applications. Let’s take a closer look at those two technologies and compare / contrast their offerings.
Opa and the Node.js: What’s the Difference?
Opa is a platform for developing Web applications. It is both a programming language and a runtime engine that tightly integrates all the required features. It uses a similar asynchronous model to that of Node.js, but it uses a completely new statically typed, compiled, functional language, designed specifically for the Web. Its development began in 2007 and is sponsored by a startup called MLstate.
Same features, same use cases — so are Node.js and Opa really similar? Not quite! There are more than a few differences and this article will focus on them.
Opa vs. Node.js: The Web Chat Benchmark
To illustrate and compare those two languages we will use the example that is slowly becoming a standard benchmark for Web frameworks/languages: a Web chat. It is a good benchmark, as it involves extensive client/server communication.
Below we present screenshots of chats developed, respectively, in Node.js (by Ryan Dahl, the author of Node.js himself) and in Opa (by Frederic Ye, an Opa developer). The Node.js chat app is a typical example showcasing the language, while the Opa chat app was written specifically for comparison. We encourage you to click on the images to play with the live application and to browse their sources. Both applications are equivalent feature-wise.
Opa vs. Node.js: Productivity
Developer productivity is a very important consideration when choosing one’s tools. If you can develop fast, it means you can innovate. You can quickly create prototypes, and you can quickly react to changing market and user needs.
There are many pitfalls of using SLOCs (Software Lines of Code) as a metric to compare productivity, but they are a good rough estimate and can give an impression of the effort required to develop something. Instead of presenting raw numbers, below we show a bird’s-eye view on the source code of the two projects, after removing comments and new lines (for Node.js sources we included only client.js and server.js, as the remaining files can be considered to belong to the standard library). The Opa project is on the left, and Node.js is on the right.
While not definitive, this example suggests that Opa is much more concise. This can be probably explained due to its more high-level nature than that of Node.js. For instance, Opa’s distribution model is providing primitives of distributed sessions that make it very easy to set-up asynchronous communications between clients or client and server. A minimal chat in Opa even fits on one screen.
Opa vs. Node.js: Building UIs
Both Node.js and Opa use (X)HTML and CSS for building user interfaces. Of course, in both cases, it’s possible to build libraries and abstractions on top of that layer but neither language enforces that and both allow direct, full control over the markup.
Node.js does not have any special support for HTML, and the markdown is simply built as a string with string concatenation (as here). There are several problems with this approach. To begin with, developers cannot perform a validation of the correctness of the markdown; unclosed tags, typos and the like will have to be noticed and corrected when testing the application. But possibly even more importantly, this poses a very serious security threat. Since HTML is treated as a string, it takes very careful development not to allow the possibility of XSS injection to slip in (where the attacker can inject and execute client-side script in the app).
On the other hand, in Opa HTML is a data type (interestingly enough it’s not a primitive type but is developed as part of the standard library of the language), with special support in the syntax that allows developers to write it easily (as here). While on the face of it, it may look only slightly better esthetically, the implications are far-reaching. Essentially all the aforementioned problems are solved: ill-formed markdown will be rejected by the compiler and the language offers built-in protection against XSS injections by properly escaping any values inserted in the constructed HTML.
Below we show equivalent snippets (taken from Node.js’ chat) in both languages.
Opa vs. Node.js: Server-/Client-side Separation and Communication
Since both Node.js and Opa are meant for both client- and server-side coding, it’s interesting to see how they both address the issue of communication between the client and the server.
Node.js provides a very explicit approach. The server and client separation is enforced at the source code level, as some files will be included in the Web page served to the users and others will be interpreted by Node on the server. Hence, the chat example contains the aptly named server.js and client.js files.
For the communication one directly uses the Ajax primitives of jQuery.
In Opa, there is no explicit separation between the client and the server part of the code. One just writes the application code, and then the Opa compiler figures out which part of the code should be sent to the client and which should be executed on the server. Of course, the developer can influence those decisions by prefixing functions with client or server directives, or one of the safety directives, such as protected (meaning the function is security sensitive and should not be trusted to the client). As a result, moving code between the client and the server is as easy as adding/changing a few directives. That’s why Opa’s chat consists of just one file: main.opa (in this example, for clarity, all the functions were explicitly annotated).
Since Opa provides this location transparency, its communication is also on a higher level of abstraction. All one does is use simple function calls and Opa’s compiler, depending on the location of the caller and the callee, and implements those as either local function calls or remote (Ajax/Comet) procedure calls.
Here is a snippet of the Node.js chat and its rough equivalent in Opa.
Opa vs. Node.js: What If Things Go Wrong?
We all know that writing the code isn’t the last step — often the “fun” begins only then with testing and debugging. Therefore, it is only fair to see how the languages compare in that respect. In fact, there are three aspects to consider here: how well the languages help detect errors, their support for debugging, and their support for testing. We will not look at testing in this article as both languages are quite comparable in that respect. We will begin by looking at debugging facilities.
On the other hand, Opa offers huge promises concerning error detection, thanks to its static typing (which in contrast to many statically typed languages does not require the programmer to annotate programs with types as Opa features almost complete type inference) and static analysis performed by the compiler. In principle, that means that it should be able to detect a large range of errors even before running the program. To test this in practice, we performed a range of tests both on the Node.js and Opa code. We tried inserting a number of errors that happen a lot in practice and seeing what happens.
GET http://localhost:8001/join?_=1327952561187&nick=akoprowski 400 (Bad Request)
and pointed to the place where the relevant Ajax request was made. It will then still take some work and debugging experience to link this error with the typo that we introduced.
We then made equivalent error in Opa’s code replacing length with lenght here. Opa would now not allow us to run such a flawed program and reported the following error (shortened here for presentation):
File "src/main.opa", line 152, characters 21-28
Expression has a record type incompatible for access to field lenght. […]
Hint: Perhaps you meant length or merge?
We then tried a similar test with user-defined data-types. We replaced text with txt in the record defining a chat message in Node.js and in Opa. The results were the same: runtime error in Node, not directly related to the problem and a clear error message in Opa.
Then we decided to see what would happen with malformed HTML, an important building block for interfaces in both languages. In Node.js in tables presenting chat message, we replaced the second closing tag with “,” which resulted in a working program (modern browsers are quite good at “fixing” markup errors) but wrong presentation. We also played with some other similar markup errors, some of them leading to difficult to spot deficiencies in the output.
In Opa’s example, tables are not used but when we introduced this very error to the snippet presented in the Server-/Client-side Separation and Communication section, we got a clear error message:
Hint: File "src/test.opa", line X, characters X-X
Open and close tag mismatch
We got similar compile-time errors when trying other variants of flawed HTML.
Finally, we tried playing with arithmetic errors. Both in Node.js and in Opa, there are equivalent functions computing memory usage in megabytes. Just to keep things simple, we replaced the second occurrence of the number 1024 with a string “1024.” We found similar results: an undefined runtime error in Ajax call for Node.js and the following compilation error in Opa:
File "src/main.opa", line 91, characters 28-41.
Function was found of type 'a, 'a -> 'a but application expects it to be of type int, string -> 'b.
Types int and string are not compatible.
While the above error message may not be very clear for somebody who never used Opa, take our word for it — for regular Opa users this is as clear as a day.
To sum up, we think those results are quite astounding. A large class of programming errors that goes unnoticed in Node.js and requires heavy testing and debugging is automatically detected in Opa and reported with helpful (most of the time) error messages. And we only scratched the surface, as Opa is actually capable of detecting much more involved problems than those presented and discussed here.
This should not be overlooked and is perhaps the most important feature distinguishing Opa. It also corresponds to something that Haskell programmers know for years: with a properly designed language it is possible to write “almost correct” programs at the first go and tremendously decrease time and effort spent on debugging and testing.