Wednesday, September 4, 2013

How to model a doorknob, functionally

Suppose you're a game programmer tasked with coming up with a way of representing doors in the game. You reason that doors have doorknobs which can be turned to open the door, leading the following typical OO design, overly simplified:

trait Door {
  def open: Unit
  def close: Unit

trait Doorknob {
  def turn: Unit

This sort of design embodies a certain way of thinking about software. A doorknob may be turned, which triggers some update to the state of the door, and we model these state changes in an entirely first order way, with no separation between the production of an effect (the state update) and its interpretation (the propagation of this state update). The view here is that actions must be produced and executed by the same piece of code.

The actor model works the same way. If we model doors and doorknobs with actors, we might have something like:

object messages {
  case object Turn
  trait DoorMsg
  case object Open extends DoorMsg
  case object Close extends DoorMsg
val door: Actor[DoorMsg] = ...
def knob(door: Actor[DoorMsg]): Actor[Turn] =
  actor { case Turn => door ! Open }

Just what is the essence of "doorknob-hood"? This is a serious question. The OO/actor response is that a doorknob is something that can be turned. But that is unsatisfying, because what it means to turn something is left unspecified--the result of turning is Unit! But what does that mean?

A more functional view of the essence of doorknob-hood is that a doorknob provides a pure function that transitions a door from the state of being closed to a state of being opened, a Door => Door. This is just one choice, but you get the idea. With this model, we're separating the production of new state from the propagation of that state. Actually running the action of turning a doorknob and propagating new states is not the concern of the doorknob.

And here are some more examples:

  • A key is not something you insert into a lock, it provides a function from Door => Option[Door] that returns a Door in the unlocked state, or fails if it's the wrong key. (And we could now state more precisely that a doorknob takes a Door in its unlocked and closed state and returns an Option[Door] in its unlocked and open state, failing if the door is locked.)
  • A timer is not something you set, it is a function from a duration to a boolean time series that will spike true at some point in the future. Who consumes this time series is not the concern of the timer.
  • A microwave is not something you turn on, it provides a function from a cold object to a warm object.
  • And so on...

Once you start seeing objects in this way, the functional view makes a lot of sense--we are modeling objects by the collection of effects they produce. Objects are not responsible for interpreting and sequencing these effects; that can be an entirely separate concern.


Unknown said...

This post feels like it's written in Haskell: it conveys so much in so few words. Thanks for it!

Unknown said...

Agreed. The problem with OO design is that you define an entity based on what you can do with it, rather than what it consists of. I think this is fundamentally broken as it severely limits how you can use the entity in your program, and it also increases dependency complexity (which I think is the single most important factor in good software design).

An entity should only be defined by it's inherent structure (which rarely needs to be changed). Behavior (which is really just stuff the developer think is useful for solving the problem at hand) should be added separately as abstractions. This corresponds exactly to Haskell data and typeclass definitions. There are however some things that Haskell gets wrong, like the type (structure) of 1, but that's an only slightly related topic. :)