Making the Case for Haskell
This was originally posted on the blog over at Gem. Check them out!
A Rubyist’s Lament: The Imperative Issue
At Gem, we are passionate about creating the world’s most reliable and secure API for developers who want to integrate Bitcoin wallets into their applications. Ruby is a fantastic and elegant language for getting this done, and paired with the actor-based Celluloid library it makes for a concurrent, rock-solid system that does exactly what we need it to. That being said, we also love exploring other ways of developing and thinking about software.
Haskell is a wonderful example of how different a language can be, and I’d like to examine some of the differences between Ruby, a language I enjoy using every day, and Haskell, a language I’ve enjoyed exploring.
Pure Functions & Global State
Back in elementary school, you ran into things like
y = f(x). Let’s create a Ruby method with the same name:
y = trainer.f(x).
class Animal def feed(x) @foods_eaten += x end end class AnimalTrainer def f(x) @animal.feed(x) x + 1 end end
We’ll call the mathematical
y = f(x) expression
P, and the Ruby
y = trainer.f(x)
f(x) does not evaluate the function
f with the parameter
f(x) is equal to
y, and vice-versa. Really think about that. There is no execution in mathematics. There is only equality and restating. You can restate
f as that function’s body, and you can restate
This concept is called referential transparency. It’s the idea that at any point you can replace
f(x), and no matter where you do it, the represented value will be unchanged – a true statement before the replacement will remain true afterwards. This is obviously the case in math.. but what about programs?
I, we certainly do not have referential transparency. This is mainly because calling
y = trainer.f(x) is not equality, it is execution. You are executing the computer code that the Ruby interpreter stored when it parsed
def f(x), then writing to the memory at location
y the value returned by
Not to mention in the
f(x) execution order we are mutating an instance variable. This is essentially telling the computer to change a block of memory somewhere.
This is fine. But it means every time you send the
f(x) command to the computer, something different occurs. It changes the state of the universe (in this case, the amount of things animal has eaten)!
You cannot replace every occurrence of
3, because the execution is vital to the correctness and expected behavior of our program. As a Ruby programmer (or programmer used to imperative), this seems normal.
But let’s add a method to Animal.
class Animal def feed(x) @foods_eaten += x end # new def run raise PukeError if @foods_eaten > 100 @health += 1 end end
If you feed the animal to over 100 and it tries to
run, it will puke (raise an error). So if you are feeding the animal with the trainer in some area of your program, then you try to make the animal run, it could puke! What if there are other ways to feed the animal? How would you debug this: the animal is puking, and you don’t know at what point you have overfed it?
You might print out animal’s
@foods_eaten at different places in your program. You might drop a
binding.pry into your program and inspect
feed to see when it was incremented over 100. (This is obviously a contrived example, but humor me.)
These debugging techniques rely on inspecting some global, mutable state. Things can have different values at different times in the program. Debugging often means stopping at specific times in the order of execution and looking around at the state of the universe. We’re used to that, but victims also get used to Stockholm Syndrome.
How doth Haskell saveth me?
I’ll respond to that with a counter-question.
How would you model this problem in mathematics?
As you can probably tell - this problem doesn’t even make sense in mathematical terms. Because remember, math is not an ordered execution of statements, it’s equality. You are not describing what to do, you’re describing what is.
So you can’t change the state on some global object and then later on read that value back to yourself.
Haskell attempts to bring this mathematical is-ness to programming.
Haskell obviously must evaluate expressions, because Haskell exists in the Real World. Math is not bound by this constraint because it is only concerned with The Ultimate Truth.
But we can break away from this concept of execution / commands (imperative programming) and move into more functional / declarative programming. In Haskell, there is no giving the computer commands. No telling the computer to mutate some globally held state. Functions take an input and return an output. As dictated by the language, there will be no side effects (no
@foods_eaten += 1).
What does this mean? It’s means Haskell is referentially transparent, because you the compiler guarantees that if
f(x) = y, any
f(x) can be replaced with
Everyone probably wants me to translate the above Ruby code into Haskell. This doesn’t really make sense, because Haskell simply does not have (encourage) the ability to create that kind of stateful object and provide commands for mutating it.
Let’s break down what the Ruby
f(x) is doing. It’s mutating the current
self, then returning
x + 1. In Haskell, without mutation, there is only input & output.
So you could do:
f :: Int -> Int f x = x + 1
Which takes an
x and adds one to it. Notice this has no notion of an
AnimalTrainer - you could try to fit one in to another function but what would it do? It would end up returning a new
AnimalTrainer, because Haskell does not allow (encourage) mutating objects, it simply returns new ones. This is a symptom of a lack of memory mutation. It is a simply of equality instead of assignment. I hope now you understand why modeling the above problem simply doesn’t make sense in the Haskell paradigm.
But it looks very similar to mathematics, doesn’t it?
f 2 = 3, and the compiler can replace all instances of
f 2 with
3, by only evaluating it once. This is not assignment, it’s equality. It’s is-ness.
If this seems too impractical for common usage, think about something we all encounter every day: HTTP requests. What is an HTTP request but a request & a response? An input and an output? In Haskell you could construct a function:
getResponse :: WebRequest -> WebResponse -- etc
No global state to deal with, just input and output.
As an aside, the more astute readers will no doubt be screaming: “the world has global state! databases! IO! WTF?”… this is a topic for a future post: How Haskell Handles IO and Ordered Execution (MONADS)
So why do all of this? Why go through the trouble of shifting your brain in order to comprehend this stuff? Why write programs this way?
They are much easier to debug.
Remember, the issues we had in Ruby were mostly caused by the state of the universe changing based on the order of execution. To debug, we breakpoint and inspect the world. In Haskell, there is none of this.
There is only what went into the function and what comes out. Testing becomes simply a matter of ensuring you get the output you desire on a certain set of inputs. It removes the reliance on context from function execution. Functions will always evaluate to the same thing, no matter what the context, which is very powerful for reasoning.
In this context, Haskell presents a simpler way of thinking about your programs. Keeping track of software you write is difficult, and adding state to the mix can make life tougher. When using libraries, often you have to understand the library as a whole in order to understand a single utility function. With Haskell, there is no global state, so a function can be used quite modularly.
Thinking about your programs as one or a few isms could alleviate the much of the cognitive burden of mentally tracking your program.
Are you switching to Haskell?
No. But that doesn’t mean we don’t love taking the time to explore new paradigms, languages, and ideas. At Gem we are passionate software engineers, but we are also software enthusiasts.
Exploring new ways of thinking allows us to examine and appreciate the idiosyncrasies inherent in current and new technologies.
Comparing Ruby to Haskell is a beautiful example of how different programming languages can be, and I think examining the differences is what makes computer science fun.