From Ruby to Haskell, Part 1: Testing
You read that right. Or maybe, if you read it as “stop using Ruby and start using Haskell”, you read it wrong. I’m going to show you why I find Haskell to be utterly fascinating and eminently practical.
Foremost, I want to collect some bits and pieces from articles that I have read and talks that I have seen. I feel like there are troves of information out there but that it may perhaps be in need of some synthesis. There are lot of meaty topics here, but I haven’t seen them collected and prepared quite to my taste yet. If my collecting will help someone else to “think different” then that’s exactly what I had hoped for.
A secondary aim is to make clear some things for myself, and again, I’m happy to take anyone else along for the ride.
But first, an FAQ
Q: Are you insane?
A: I don’t think so. It is just that my mind keeps wandering into
foldr, etc. There are also some really fascinating ideas on how to use concepts from type-class polymorphism rather than subtype polymorphism (I’ll cover that in a later article) in designing things like the DCI architecture
Q: Do you expect me to believe that there are more than zero people that will move from what is perhaps the most dynamic language to what is perhaps the most static?
A: I don’t know, but also, I don’t think that it matters. Or at least it doesn’t matter in the way that you may think. Telling people what they should do is not my style. I will cite two sources.
In “How to Become A Hacker”, Eric S. Raymond writes: “LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot.” Clearly, he was talking about lisp, but I believe that the lessons translate over to Haskell.
I think static vs. dynamic is the wrong focus. As programmers, I think we’re fundamentally concerned with the elements of programming laid out in “Structure and Interpretation of Computer Programs” (SICP):
- What are the primitive expressions?
- What are the means of combination?
- What are the means of abstraction?
Or maybe, “what things can I talk about, how do I put them together, and how can I black-boxify them?” That, for me, is the real guts of programming. Different languages have different answers to each of those three. Ruby may say: “1. Objects! 2. Inheritance and mixins 3. Objects! (and methods)” (feel really free to disagree)
Q: Why do you hate objects? And freedom?
A: I don’t! It’s not about the objects, it is about how those objects interact. If you strip objects of their behaviors, then what you’re left with is just data. What separates our programs from XML, YAML, JSON, or SEXPRS is just the way that we consume and change that data. An argument can be made that there is an unseemliness about objects, that data and behaviors are co-habitating when they shouldn’t be, but I think modern style is moving away from this. Mixins are just the thing for abstracting away behavior and making objects svelte containers for data.
Q: But I heard…[mumble] hacker news…
A: I know, I heard that too. But take a look anyway.
Testing. Sigh I don’t have any flamewar-resistant underpants, so I’ll have to tread carefully. Testing is a hot topic because it is a big raw nerve whose other end is connected to how do we write reliable software?
I think the best way to show this off is to demonstrate
So then I can “implement” my feature like this (undefined has whatever type is needed to “make it work”): when I run that I get the pleasing “red” step that we all love:
1) absolute returns the original number when given a positive input FAILED
2) absolute returns a positive number when given a negative input FAILED
3) absolute returns zero when given zero FAILED
Prelude.undefinedFinished in 0.0005 seconds, used 0.0006 seconds of CPU time
3 examples, 3 failures
it gets even fancier with
hspec-expectations, giving you
shouldThrow; they probably do what you’d expect.
Something that may not be as familiar as TDD/BDD is property-based checking. If you’ve ever used the Rushcheck library for Ruby (only about 5,000 of you according to Rubygems), you set out some property of your function that you want to hold and then the test framework tries to disprove that. If it can’t find a countrexample, then you can be more confident that it is working as intended.
In Haskell, the granddaddy of property-based testing is called QuickCheck. I’ve modified my test file to look like this:
Note that I’ve added
mathCheck, it calls
verboseCheck, if you want that) on a property, here I’m saying that for any
n, the result of calling
absolute n should be greater than or equal to zero. Remember that the form
(\n -> absolute n >= 0) is an anonymous function that takes an
Int and returns
Bool (true or false). The only other change is that I’ve modified
main so that it runs both types of test, the spec and then the quickCheck test.
Finally, I’ll try and implement my
Okay, I’ll run all my tests to see if it works:
Cool, now both my property-based tests and my specs pass. I now have a slightly more optimistic outlook about my
absolute function. That’s paranoid programmer speak for “it works!”
Referential transparency, side-effects, and testing surface
Let me finish this installment with a little talk about something that may not quite be testing, but surely has a big effect on testing:
I’d argue that
foo1 is better for testing than
foo2 is better than
foo2 both are both referentially transparent — you could replace their call with their value:
foo3 would be a little bit trickier. You’d have to know what was going on with
$global_frob at the time you wanted to replace
$global_frob was true at the beginning of your program but then became false later on, then
foo3 would take on different values.
This is dependence at its minutest levels. Even relying upon some global state starts to cause problems when you want to test. To test something stateful, you must first create that state around that piece of code and then immerse that code into that prepared state. The code needed to set up that state has nothing to do with the code being tested, it is just the environment.
When I’m writing tests, a lot of what I spend my time on is thinking “how am I going to break these dependencies so that I can unit test in isolation?” It can be devilishly hard to come up with a good seam in the code I’m trying to test and then mock or stub out the unrelated code.
Functional code helps this situation a great deal. It reduces the “testing surface”. The functional code is very modular and cannot produce any effects other than returning a value (and it’ll always be the same value for any input). The “seams” are always at the boundary of each and every function.
I wanted to give a sense that there are excellent testing libraries available in Haskell and that you won’t feel totally lost from a Ruby perspective. And I wanted to provide a stepping stone that ties them together.
That’s not to say that there are not other, perhaps more emphasized means of achieving code quality in Haskell. You should learn to lean on the type system and move those things that you can into the realm of types. Certain kinds of errors can be eliminated or drastically reduced by moving them into types.
As a quick example, imagine two distinct types: HTML and String. The type system will not let these be used interchangeably. If your output functions all require HTML and not String then you can guarantee that what you’re rendering to the browser has been through some sort of
convertToHTML :: String -> HTML function before anyone has a chance to see it. Any forbidden characters can be escaped on the way to creating the HTML type. See the references for a much better explanation.
References (by section)
- How to Become a Hacker
- A Type Driven Approach to Functional Design – Strange Loop 2012 Session and Video (wasn’t available at the time of writing)
- Elements of Programming – Structure and Interpretation of Computer Programs
- Bugs and Battleships
- Why Haskell is beyond ready for Prime Time
- Type Classes and Overloading – A Gentle Introduction to Haskell
- Exercise 44: Inheritance vs. Composition – Learn Ruby the Hard Way
- HSpec – Behavior-Driven Development for Haskell
- Designing Type-Safe Haskell APIs
- A type-based solution to the “strings problem”
- Functional Core, Imperative Shell