A Hint of Hubris
Ruby is a highly dynamic language with impressive capabilities for runtime redefinition of classes, objects, methods, and variables. Haskell, on the other hand, is a purely functional language that confines mutation within a sophisticated static type system. Given their many differences one or the other may be more suited for whatever problem you might be working on (see polyglot programming), but sometimes, a mix of both would be even better. Thanks to Hubris, you now have that option!
Hubris is a gem that allows you to call out to natively compiled Haskell shared objects. In this post, I’ll talk a bit more about why you might want to do that, how you’d go about doing that, and share some real world examples.
So Why Would I Want To Use Haskell?
It’s easy for any decent programmer to spot the many striking differences between Ruby and Haskell, but why a Rubyist might be interested in Haskell is a question worth asking. There are two primary scenarios: ###1. Native Extensions When CPU-intensive algorithms are needed for a task in Ruby, you usually drop down to the C or C++ level to take advantage of the highly optimized code yielded by a C/C++ compiler. Unfortunately, for someone without significant experience in the language, it can be easy to run into memory management woes, not to mention the frustration of dealing with a more hands-on (with respect to memory and explicit typing) language like C.
Haskell is a managed memory language with one of the most powerful type inference capabilities around. What this means is that you can avoid being bothered with explicitly denoting types at function call-sites, and choose whether or not to add types to definitions for the particular function you’re working on. Additionally, GHC (Haskell’s most prominent compiler for production applications) produces compiled native code that has performance in the same league as C/C++. ###2. Concurrency Many popular languages (especially Object Oriented ones) allow unconstrained mutation of data (e.g. mutator! methods in Ruby). This has always had implications for the difficulty of reasoning in the resulting code. But lately, this has become a bigger problem because it makes reasoning about concurrent program execution a nightmare.
What differentiates Haskell from many other functional languages is that Haskell isolates side effects statically in types rather than dynamically in language constructs. In effect, this means you can statically determine a list of functions/places to look (and eliminate many of the places you don’t have to look) when problems related to side effects do come up. ###3. And More Haskell as a whole has many other useful properties, such as implicit laziness, implicit argument currying, a typeclass system for generic programming, various concurrency constructs including an STM and a wonderful concurrency debugger… just to name a few. Rubyists who enjoyed Why’s Poignant Guide to Ruby may enjoy parsing through Learn You a Haskell for Great Good.
Okay, I Buy It: Now How Can I Do It?
Now that we’ve seen why you might want to use Haskell, we’ll take a look at how to use Hubris from Ruby. There are two main modes of operation: a way to embed a Ruby string containing Haskell code inline, and a way to load code from a plain Haskell .hs
file. Like so:
class Example
hubris :inline => "foo :: Integer -> Integer; foo x= 1330 + x"
end
Example.new.foo # => 1337
class Example
hubris :source => "MyHaskell.hs"
end
Example.new.my_awesome_function "go ye forth into a world unknown"
Note that due to support for the :inline
option, you can even experiment with adding new functions dynamically in an irb session (compile errors like type conflicts get spit out for you to read)! In the background, Hubris will compile code to /var/hubris/cache
and then link it in to your running Ruby program.
The first time you run the code, it will be a bit slower as it compiles the Haskell, but subsequent runs will load the cached .so
files directly. Another nice property is that the already compiled code is compared against a saved hash in order to only recompile when changes have been made.
As the Hubris README demonstrates, a third option is load modules already installed on the system:
hubris :module => "Data.ByteString", :packages => ["bytestring"]
Hubris comes with an example Rack app that exposes a Haskell implementation of the Fibonacci function. I’ll show it to you here as a compact Sinatra app. A likely use case for Rubyists will be to leverage Ruby’s mature web development library ecosystem by exposing an API for a compact and optimized Haskell algorithm/program .
require 'sinatra'
require 'hubris'
helpers do
hubris :inline => <<-HASKELL
fibonacci :: Int -> Int
fibonacci n = fibs !! n
where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
HASKELL
end
get '/:number' do
fibonacci(params[:number].to_i).inspect
end
As you can see, sensible conversions between data types in both languages are done automatically. Below is a table of the mappings between Ruby and Haskell types (see the Hubris specs for example uses). Try searching Hoogle (no, that’s not a typo) to find Haskell functions supporting types you’re interested in.
Ruby | Haskell |
---|---|
TrueClass/FalseClass | Boolean |
Fixnum/Bignum | Integer |
Float | Double |
String | Data.ByteString |
Array | List |
Hash | Map.Map |
_value_ or nil | Maybe |
[Not yet](http://github.com/mwotton/Hubris/issues#issue/15) | Multiple arguments |
[Not yet](http://github.com/mwotton/Hubris/issues#issue/16) | Tuple |
[Not yet](http://github.com/mwotton/Hubris/issues#issue/16) | Algebraic types |
Hubris currently lacks support for calling functions with multiple arguments (or tuples or custom algebraic types)—which is a serious drawback. However, this is not by design and you can track the Github issues for those missing features via the links in the table above.
In the future, fine-grained interaction between both languages will be possible. For example, unit testing Haskell functions with RSpec, including those with multiple arguments. For the time being, you can take a more coarse-grained approach by passing a hash of many values to a single interface function in Haskell.
A Real-World Example
I’ll end my overview by linking to a more complicated example that exposes a Ruby webservice to a coarsely-grained Haskell function. Here, a Sinatra web service is passed a JSON string, which is parsed into a Ruby Hash (actually using C via the JSON gem), then passed to a Haskell function, which is converted on the Haskell side to a Map and then returns an [[Int]]
List that gets converted into a nested Ruby Array of integers to finally return the web response as a String. (That made for a long and hard to read sentence, but go through it slowly: it makes sense.)
Rather than calling out to a simple Fibonacci function, you call a basic implementation of a Multi-layer Perceptron Neural Network. HUnit is used for unit testing in Haskell. These tests correspond to values in a handwritten derivation of the first epoch of learning the XOR problem. A Ruby integration test drives the whole stack via rack-test on the XOR problem. A rake task runs both sets of tests to conveniently validate expected behavior. Explaining the algorithm would be enough content for at least a separate post, but hopefully Neurosis can serve as a useful Hubris reference implementation to fiddle around with in the meantime.
Conclusion
There are some serious limitations that prevent fine-grained interaction between Ruby and Haskell. Nevertheless, Hubris has strong potential as a useful library and can be used today via coarse-grained interfaces. Note that Hubris has been tested to work on MRI 1.8 and 1.9, but should work with any Ruby that conforms to MRI’s C API.
Installation of the Haskell-side of the library is a bit of a pain right now, but this wiki page is recording the team’s progress as they work on fixing it.
So here we are: it’s time to go out and write your own Haskell code, to be called from Ruby (or find a favorite Haskell library to reuse)… that is, if you can make it past installation ;)
As always, leave any questions and comments!
Share your thoughts with @engineyard on Twitter