If you ask ten people to define object-oriented programming, you’ll probably get ten different answers. While it’s probably not one of the defining features of OO (my answer: dynamic dispatch, object identity, and yes, mutable state), certainly one of the central concepts is encapsulation.
It’s pretty easy to see why encapsulation is useful generally (although it can become muddled when people start piling on getters and setters.) So one of the major puzzlers that an OO programmer might experience when trying to actually use a functional language (like Haskell, OCaml, and F#) is that you don’t see encapsulation at first. In fact, algebraic data types seem to deliberately and completely violate encapsulation!
And… it all seems to work out? How should the OO programmer make sense of this, when it’s often deemed extremely important in OO land?
One possibility is simply that programmers are encouraged to give types stronger meanings. Ask programmers how to represent money. A decent programmer is going to say “an integer.” A poor programmer (for most domains) will say “a float” or “a string!” A Haskell programmer might reply “Cents.”
newtype Cents = Cents Int
Since the immediately googlable explanations of newtype are terrible, I’ll given one of my own here. Typically, any given type from a library has awful semantic content. An ‘Int’? Of what? In what units? The type doesn’t say. An ‘[Int]’? What does that mean, besides a list of integers? The more you have to rely on context to get meaning, the more likely a programmer is to make mistakes from missed or forgotten contextual information.
Newtype lets you create a type, that you can then decree to have some semantic content. ‘newtype Cents = Cents Int’ and then document the meaning of the type as “cents in US currency.” This new type then carries it meaning explicitly, instead of implicitly as context the programmer must remember.
And it’s not just a type alias. You’re forced to do a little song and dance to make the type checker happy, reminding yourself of this meaning everywhere you use it by wrap/unwrapping (zero runtime cost, but you’re not supposed to care yet) what you’re working with. For example, spot the errors in the following context-less piece of code:
addDollars :: Cents -> Int -> Cents addDollars (Cents cents) dollars = Cents (cents + dollars)
But consider if it had been
addDollars :: Cents -> Int -> Cents addDollars (Cents asdf) qwerty = Cents (asdf + qwerty)
Choosing descriptive variable names is wise. But choosing descriptive types may be more important. All we’re missing is a Dollars type, and the awful variable names here wouldn’t have even mattered.
The benefits of this start to compound significantly once you’re working with data that’s aggregated together somehow. Ponder the difference between these two functions:
newRect :: int -> int -> int -> int -> Rectangle newRect :: Point2D -> Width -> Height -> Rectangle
It’s compiler-checked documentation. It immediately raises a possible code smell any time you see ‘newRect (Point2 x y) (Width h) (Height w)’ in your code. It’s damn invaluable as a refactoring tool. And so on.
No null, yes garbage collection, no mutation
I think, in this century, most people can appreciate the advantages of garbage collection. Without it, ANY sort of interesting data structure must somehow suffer from managing state: whether any given pointer is currently valid, invalid, or null. “Who” is responsible for freeing a pointer, once allocated, that implicitly propagates through the program. And so forth. Obviously, not having GC increases the frailty of the data representation, and it’s then extremely helpful to encapsulate the data to help manage these invariants. (Of course, then there are things like the various smart pointers in C++ that can help deal with these small concerns.)
But, even in a language like Java, any reference may still be null, and this still presents many of the same state-managing problems to a data structure that might not need it otherwise. After all, if full implementation details are exposed, then the user is perfectly capable of constructing a data structure with nulls in inappropriate places. And frequently, there’s no indication of what’s an appropriate or inappropriate place, except that some will raise null pointer exceptions sometimes and others will be handled just fine.
Or worse, given that OO languages typically have unrestricted mutation, they might modify the content of a data structure in any way — and this part is important — invisibly. Call a function, pass it a reference to a data structure. Will that data structure get modified?
In a functional setting, of course not! Even in the ML family, which does not require stateful functions to involve monads or anything of the sort, you know that a value of a type without any ‘ref’s in it won’t change. In Java it’s hopeless, but in C++ you might get away with ‘const.’ If your users don’t hate you for using const, because there’s so much const-incorrect code out there they have to work with. Oh well, they can just cast it away, hahahaha… sigh.
The end result of all this is that the values of a type in a functional language are actually just values, and not state representations. Yes, it’s still quite possible to construct nonsense. But, it’s a lot more obvious when and where values are getting constructed. There are stronger invariants on what a value might look like (we only have to worry about the values that a value is composed of, not null references, not pointer ownership). And with newtype, we can be sure those values have an obvious meaning to the programmer who is using or constructing them. To construct nonsense, you pretty much have to know you’re constructing nonsense.
Another somewhat interesting aspect of why this works is that (duh?) the features to make it work are also present. Any time you have a data structure where a value may be one of several different constructions, an OO language forces you to make use of subtyping (each construction is a child class.) And your base type typically has few internal details, instead there’s simply a set of functions the subtypes should appropriately implement, and you don’t know which subtype it really is, so you don’t have access to those details. You’re almost forced into encapsulation.
Algebraic data types, on the other hand, fix the list of constructors, so the details are right there. And pattern matching actually makes using those details quite pleasant. I’d wager that nearly all situations where we see class with a lot of getters/setters is an example of a situation just screaming out for data types instead of objects.
Finally, we do encapsulate!
Because it is actually a good idea! The major difference seems to be that the OO-style forces you to encapsulate immediately, which isn’t always the best thing. All manner of crap has been invented in the OO world to violate encapsulation in limited ways. Java’s “default” protection modifier, unfortunate things like ‘friend’ classes, sometimes reflection, and so forth. The public method or class with documentation that just says “internal API – do not use” is hardly unheard of!
The unit of encapsulation isn’t always one type! So these things become necessary.
In a functional setting, you work with internal details exposed… until you hit a boundary (often a module) where you decide it’s time to hide it. Then you have a type class, or a module signature (depending on whether you’re in a Haskell-like or ML-like language. Have you ever been in a language?) that defines an interface, much as you would see in an OO interface.
Then, your module hides the implementation details. Users of the module don’t see what they shouldn’t see. Tadaa!
The bottom line
A common theme celebrated by functional programmers is that their programs “just work” after the type checker is satisfied. I think the biggest part of this is controlled state, and I think many would agree with this.
But I think the second biggest part of it is this exposed representation — internally to a module, of course. One of the problems with encapsulation and abstraction is that we nearly always end up making abstractions leaky. Real programs are messy, there are unfortunate corner cases, programmers makes mistakes, good design is hard, and so on and so forth.
When you’re working in a module with exposed representation, you’re basically programming at the “metal” of what you’re really doing. (Pause a moment to let the C programmers in the room stop laughing at my use of that metaphor for writing Haskell, please.) There aren’t any leaky abstractions, except perhaps those from other modules you’re using. What it is, is what it is. It’s not bytes and pointers, and you may not know what the exactly assembly generated is, but you can have an equally good (or better!) idea of what’s going to happen when you run it (if you’re willing to temporarily ignore a couple of important operational details, like how much memory it will consume. Correctness first, optimization second!)
As a result, not only are you less likely to forget some detail and mistakenly trust a leaky abstraction, because what you’re actually doing is right there in front of you, but the language has been designed around helping you remind yourself of all those details.
Why am I thinking about this
So that’s 1400 words I hope is interesting to someone.
I’ve been thinking about all this recently because I’m working on extensible languages, specifically the meta-programming language Silver. (We’re also in the very early stages of building a google code project site for Silver. That link probably isn’t useful now, but probably by 2012…) One of the potential pitfalls is that, to really allow a compiler to be extensible, a lot of the implementation details need to be exposed so an extension can… well, do it’s thing with them. So a major question we have to be able to answer is: why isn’t this just a completely horrible spaghetti code scenario?
My hope is that we can get enough compiler- and language-enforced properties that:
- We can completely and totally rule out MANY serious ways in which an extension can misbehave. Much like functional languages do with purity and data types, but even stronger properties are needed.
- We can be confident that, for the few ways that are impossible to rule out, we get the same kind of “experience” that functional programmers do living dangerously with internal implementation details. That is, making sure those details are brought to the programmer’s attention when coming anywhere close to them.
- We can automatically gather ALL relevant information about a point of possible misbehavior, not missing any important details, so an IDE, for example, can display it for the programmer. Or just so the programmer has some straightforward idea of where to look for these things themselves. Or at the very least so we have some hope of categorizing them so they can be tackled by a few standard coding conventions.
- We can still do encapsulation when appropriate.
Of course, the real goal is to make language extension seamless and problem free for the user of the extended language. If the user can’t be sure that trying to use extensions X and Y together won’t make the compiler explode, it’s probably game over for trying to convince anyone to use extensions at all.
But much like laziness was a kind of “forcing function” for purity in Haskell, I suspect making sure things can’t devolve into spaghetti code will be a valuable “forcing function” for ensuring language extensions will “just work” for the end users.
Okay, now that’s 1900 words I hope is interesting to someone.