Let’s hear it for REPLs

September 24, 2012 at 9:00 am
filed under Coding
Tagged , , , ,

Hey, let’s hear it for REPLs. I realized that I take them for granted, right up until one of the Lisp books (probably On Lisp) went on about iterative development.

Now, I wonder if in part this has been because, of the languages I’ve spent the most time with in the last few years, most of them have been interpreted or dynamic languages, like JavaScript, Ruby, or Python. With Haskell and Clojure, my experience has been similar.

Iterative development

I had almost forgotten that there’s any other way to do this with a “real” language.

The way this works, at least at the beginning, is through play. You explore a new funciton or language by trying new things out and seeing what works. Then you try to relate that to what you know next. It’s a very natural way to play around with things. The difference between typing it out into a file, compiling, and running is that the feedback is immediate. Type, enter, done.

Once you’re relatively comfortable with a set of constructs, you start composing them. This is where I feel Python parts ways from functional languages, albeit more subtly.

Languages which emphasize functions — especially purity — require a lot less setup before you can play with them. The often-used Lego analogy is very apt. Via idioms, these languages encourage you to snap together small pieces, which you then snap on to other medium or large pieces. This is possible because functions do not perform actions as much as they perform transformations; it’s exactly like composing console commands.

Frankly I think I’ve been shocked at how much better functions are as building blocks, particularly in Clojure, when combined with simple data structures. Objects just don’t compose nearly as well. “Proper” OO design aggregates data and functions into fiefdoms which obscure data and mechanisms. And reason you have to do this is because of pervasive state; you want to make sure nobody messes you up, so you set up objects as gatekeepers. But an object two levels down knows nothing about one two levels up — again, by design — which puts you in the position of having to write new code any time you want those pieces to interoperate.

Conversely when you are building with pure functions, you have access to as much of your program as you like because you don’t need this protection by default. And it’s actually counterproductive to hide your data behind anything much more complex than a simple map. You can build a flat hierarchy of abstractions which you compose (functions) rather than encode abstractions in a lattice of inheritance. All this adds up to speed and flexibility.

Of course, you can still prototype out some function calls as you work at a Python interpreter. Actually, I found that concerns about setup and testability are precisely what drove me towards a more functional (pure functions) style. Declaring and setting up objects in a Python REPL is a pain, but it’s not so bad for functions. And if you want your code to be easily testable, it helps to avoid state and instead reply on explicit input/output. Sound familiar? Maybe testing is another reason why purity didn’t require much of an adjustment for me.

Clojure

With Clojure (as with any Lisp, possibly), I find iteration is faster yet.

For one thing, I don’t have to worry about the compiler, as opposed to Haskell. (This may or may be worth the trade-off, but I’m not working on “real” software right now.) In the REPL, you have access to the entire language; you’re not inside the IO monad or what have you.

For another, I’m somewhat surprised to say that I find s-expressions easier to get right. Haskell’s syntax is a different flavor of elegant — I still prefer Haskell syntax for function composition — but when you can’t rely on . and $ to save you, you resort to the dreaded parens, or where clauses.

This is ultimately an argument about syntax, so I won’t dwell on it much longer. Suffice it to say that it is another trade-off: simple, regular syntax in the form of s-expressions, versus something more flexible which does not nest or compose as well, or at least as naturally.

Everything else

What’s funny, I guess, is that many industry-standard languages have either hacky or silly ways to simulate a REPL. I mean, Haskell is as formal and static as you get, but it still has a REPL. C++ has no REPL to speak of; the best I saw was a Python script which just called a compiler, and not particularly well. Java has a couple of hacks — Rhino or Eclipse scrapbook pages — but for sufficiently advanced projects, I found them inadequate. If you’re blessed with an existing unit test suite, that’s probably the best you’re going to do.

I have to call out Go here, though, as an example of a fantastic compromise. It does not have a REPL as such. You can’t just drop in your project and go to town, so that’s a drawback. But I’ve lost count of the number of times I’ve started with code in the playground before I ever opened a file on my own machine. In practice, the Go Playground is 75% – 95% of what you need in a REPL.

REPLs 4 Life

REPL-style development is something I’ve taken for granted ever since I started messing about with dynamic languages like Python. People ragged on dynamic languages for many reasons, but people were willing to make a similar trade-off — expressivity at the cost of perf — at Java’s inception. (Of course, there is the issue that, in practice, languages with REPLs tend to be dynamically typed, which is its own debate.)

If you haven’t played around with a language with a REPL (or something like Go’s playground), especially a functional language, I would highly recommend it.

%d bloggers like this: