In my day job I work in a lab environment where we constantly experiment and explore. We recently did a project where we had several calls out to different APIs. Some of the calls were dependent, and some could be concurrent. We typically reach for Ruby when we need to stand up a server because it’s a language that we can move quickly in. When you work on several different projects in a quarter, moving fast is something we value and optimize for.
However, Ruby is not good at concurrency — at least not in the way we needed for that project. We reached for Scala in that project, and it worked great. One of our team members has done a fair amount of work in Java-land and Scala, so we had a good bridge and it was pretty fun.
When that project wrapped I was asked to take a look at how that server would look in a couple of alternative languages. That kicked off some conversations that ended up with this apples-to-apples comparison across several languages.
The project for this experiment is a simple one that is the foundation of most of what we do on the back-end in the lab: consume some API and do something with the JSON response.
I created a simple Rack app. Rack is a very thin ruby server. The app serves a JSON payload with a string, integer, floating point number, and date. It’s not a fancy multi-threaded webserver, and it’s running locally. The response time for a single request is
0m0.030s (according to
time curl http://localhost:9292).
Here is the server repo: https://github.com/barrettclark/language-comparison-server
I wrote clients in the following languages:
* Scala (Finagle)
* Swift 2.0
I think all of these languages handle concurrency to one degree or another. I didn’t really test that aspect of the languages in this exploration. This was more about just getting up and running, and seeing how it felt to just issue the request and handle the response. For the most part these are not languages that I have actually used.
That bears repeating: I don’t really know most of these languages. I’m just some guy who made a simple thing in a lot of languages. I wanted to see how easy it is to get up and running with these. The example isn’t completely contrived, but it’s also not necessarily how you would actually do stuff in the given language. I’m OK with that. I was looking to write code that was as idiomatic as I could muster using the standard lib to the extent possible to compare as apples to apples as possible.
And with that, I give you a big table:
|Language||Typed||Compiled||Paradigm (Wikipedia)||Ceremony||Project Setup||100 Requests|
|Go||Yes||Yes||Imperative||Low||Manually create file(s)||0m1.359s|
|Python||No||No||Functional, OO, Imperative, Procedural||Low||Manually create file(s)||0m7.319s|
|Rust||Yes||Yes||Functional, OO, Imperative||Low||
|Scala||Yes||Yes||Functional, OO, Imperative||High||Manually create file(s)||0m3.978s|
|Swift||Yes||Yes||Functional, OO, Imperative||High||Create a new Xcode project (for now)||11 ms|
Running the benchmarks:
lein uberjar && time java -jar target/uberjar/client-0.1.0-SNAPSHOT-standalone.jar
time ../../repeat.sh mix run lib/client.ex
go build && time ../repeat.sh client-go
time ../repeat.sh node index.js
time ../repeat.sh python client.py
time ../../repeat.sh target/debug/client
./sbt universal:packageZipTarball && tar xzvf target/universal/client-1.0.tgz && ./repeat.sh ./target/universal/client-1.0/bin/client
A note on Clojure and Scala: I created loops that iterated 100 times inside the compiled code. The JVM startup time is about 2 seconds, so calling into the compiled code cold each time incurs that additional cost. The total time for each was just over 4 minutes when you include the JVM startup in each of the 100 calls.
Similarly, with Elixir, each time to run
mix run it fires up the Erlang VM. The total time for 100 full executions was 1m11.299s.
Ceremony refers to the number of hoops you have to line up in order to make code in that environment. A JVM-based language definitely has a lot of hoops to jump through. Dealing with Xcode is a lot of ceremony. Node, with all it’s package and library management, is a lot of ceremony. Rails is also pretty high ceremony now, but I didn’t use Rails in this experiment.
Some Thoughts On All These Languages
I’ve never done LISP. I didn’t use RPN on my HP calculator. I’m not really all that into Java and find that ecosystem a symphony of sadness. In fact, while playing with Clojure I updated openssl, which totally hosed the entire Java ecosystem on my laptop. Leiningen couldn’t pull packages from the Maven repo. I reinstalled Java, and all was right again.
If I worked with someone who was into Clojure I’m sure that would help a lot. Community is super helpful. I don’t, though, and it hasn’t really grabbed me yet.
I first looked at Elixir a couple of years ago when Dave Thomas got excited about it and released his book (as an eBook originally). It was my first taste of functional programming. I did a few exercises on Exercism, but never had a practical application to build. I’ve wanted to come back to it.
On the first pass through this exercise I used
HTTPoison to issue requests and
Poison to parse the JSON. I found them extremely cumbersome, and felt like maybe Elixir wasn’t for me.
I mentioned that I found Elixir cumbersome on Twitter, and immediately had a handful of replies asking why. They were genuine questions, wondering what it was that didn’t work for me. Remember when I said that community is important? Elixir has a fantastic community who cares deeply about making the language feel good — to the point that confusing error messages are considered bugs.
I took another look with a more simplified solution. It felt good. I also had a handful of people contribute to that repo, which was I really appreciated.
At first Go didn’t really strike a chord with me. It felt weird how simple and small the language was. I missed having constructs like
reduce when working with a collection. Go relies heavily on pointers.
Side effects are a first-class citizen in Go.
Go is also the only language I’ve worked with that cares where you put your code.
However, I think I’ve come around on the language. It is simple, and incredibly powerful. It’s designed to help developers move fast. I want to do more with it. It’s also really easy to parse JSON in Go.
I come from a Ruby background. Convention is a big thing in the Ruby community. I’ve found it really hard to discern a convention in Node.
I wanted to go with each language’s standard lib to the extent possible, so I stayed away from Django. I still don’t know how to actually make a “python project”.
The code was pretty easy to write. The standard library looks pretty powerful. The onboarding documentation is a bit of a wall of information (once you find it). It’s a fine language. Being a rubyist, I would probably stick with Ruby when I wanted to use a scripting language, but I believe in horses for courses. There is definitely a place for Python.
I was blown away with the Rust onboarding documentation. This is another language where you can tell that the community really cares. They also consider confusing error messages a bug in the Rust community. The language is still really young and changing rapidly. It’s been fun to play with.
Scala, like Clojure, sits on the JVM. It uses a slightly different build process (
sbt) that worked better for me than
lein. I also work with someone who knows Scala, and that definitely helps take the pain out of lining up the pieces.
From my brief experience with Scala, the base standard library is really just Java. Idiomatic Scala seems to come in with additional layers. We used Finagle at work, and I ended up using that in this experiment.
There is a lot of ceremony and manual setup, but once you get the pieces in place getting and parsing JSON is really nice (especially with Argonaut). A downside of the Scala/Finagle layer cake is that documentation can be a challenge. It was hard for me to discern where to look and how to ask the right questions in The Google.
Later this year we will be able to run Swift on servers. It’s a bit of an odd fit in this comparison. You can run it from the command line, but you wouldn’t. I wasn’t able to get a good time benchmark from the command line, either.
Still, I really like the Swift language. Swift 2.0 has some good improvements. I’ve tried several different approaches to making HTTP calls in Swift. I used
NSURLConnection.sendAsynchronousRequest:queue:completionHandler: for a long time. I’ve used Alamofire, which is nice but a really big library. SwiftHTTP is also a nice library that’s a lot simpler. JSON parsing can be done several different ways, too.
A pattern that I really like uses the promises/futures pattern. BrightFutures is a pretty nice library for that. Add in Thoughtbot’s Argo (like their Scala Argonaut) library to parse JSON, and you’ve got a really nice setup.
Xcode still struggles when you go too far into the FP rabbit hole, so there’s that. Still, I’d much rather build a native app than a responsive HTML site, in part because I really like Swift.
It’s fun to look around and see what other communities and languages are doing. I’m still not really comfortable in a FP world, but I like it.
I think my Power Rankings (in order of what I would want to use on my next back-end project) from this experiment are:
- Scala Finagle
- Go or Elixir
I’m actually torn between Finagle, Go, Elixir, and also Rust. I work with people who do Scala and Elixir, so they have a bit of an advantage. I also really want to play more with Go and Rust. You can deploy both Go and Rust to Heroku, so maybe I’ll sneak them in on a smaller project. Swift doesn’t make the list yet because it doesn’t run on the server — yet.
This Is All Arbitrary
A final thought about all this. I did a very simple version of a very common task that I do at work. The benchmarks aren’t perfect. What do I even know about any of these languages? This was an experiment. I appreciate that I get to do these sorts of things.
The next time a project comes up, we will have some more talking points to help guide technology decisions, and maybe you will find this helpful as well.