Monday, October 18, 2010

Phantom Types In Haskell and Scala

/*
Some literate Haskell and Scala to demonstrate 
1) phantom types
2) I am a masochist
3) ???
4) profit!

The code is probably best viewed with a syntax colorizer
for one language or the other but I've colorized all my
 comments.

> {-# LANGUAGE EmptyDataDecls #-}
> module RocketModule(test1, test2, createRocket, addFuel, addO2, launch) where

*/
object RocketModule {

/*
None of these data types have constructors, so there are
no values with these types. That's okay because I only 
need the types at compile time. Hence "phantom" - 
ethereal and unreal.

> data NoFuel
> data Fueled
> data NoO2
> data HasO2

*/
   sealed trait NoFuel
   sealed trait Fueled
   sealed trait NoO2
   sealed trait HasO2

/*
The Rocket data type takes two type parameters, fuel and
o2.  But the constructor doesn't touch them.  I don't
export the constructor so only this module can create
rockets.

> data Rocket fuel o2 = Rocket

*/
   final case class Rocket[Fuel, O2] private[RocketModule]()

/*
createRocket produces a rocket with no fuel and no o2
 
> createRocket :: Rocket NoFuel NoO2 
> createRocket = Rocket

*/
   def createRocket = Rocket[NoFuel, NoO2]()

/*
addFuel takes a rocket with no fuel and returns one with
fuel.  It doesn't touch the o2
 
> addFuel :: Rocket NoFuel o2 -> Rocket Fueled o2
> addFuel x = Rocket

*/
   def addFuel[O2](x : Rocket[NoFuel, O2]) = Rocket[Fueled, O2]()

/*
Similarly, addO2 adds o2 without touching the fuel

> addO2 :: Rocket fuel NoO2 -> Rocket fuel HasO2
> addO2 x = Rocket
 
*/
   def addO2[Fuel](x : Rocket[Fuel, NoO2]) = Rocket[Fuel, HasO2]()

/*
launch will only launch a rocket with both fuel and o2

> launch :: Rocket Fueled HasO2 -> String
> launch x = "blastoff"

*/
   def launch(x : Rocket[Fueled, HasO2]) = "blastoff"

/*
This is just a pretty way of stringing things together,
stolen shamelessly from F#.  Adding infix operations is
a bit verbose in Scala.

> a |> b = b a

*/
   implicit def toPiped[V] (value:V) = new {
      def |>[R] (f : V => R) = f(value)
   }

/*
Create a rocket, fuel it, add o2, then 
launch it

> test1 = createRocket |> addFuel |> addO2 |> launch

*/
   def test1 = createRocket |> addFuel |> addO2 |> launch

/*
This compiles just fine, too.  It doesn't matter which 
order we put in the fuel and o2
 
> test2 = createRocket |> addO2 |> addFuel |> launch

*/
   def test2 = createRocket |> addO2 |> addFuel |> launch

//This won't compile - there's no fuel
 
// > test3 = createRocket |> addO2 |> launch

//    def test3 = createRocket |> addO2 |> launch

// This won't compile either - there's no o2

// > test4 = createRocket |> addFuel |> launch

//    def test4 = createRocket |> addFuel |> launch

// This won't compile because you can't add fuel twice

// > test5 = createRocket |> addFuel |> addO2 |> addFuel |> launch

//    def test5 = createRocket |> addFuel |> addO2 |> addFuel |> launch
}

Friday, October 15, 2010

Systems Design: Making a New Feature (Nearly) Too Much Trouble

EDIT: Since I wrote this BART has finally implemented an integration between parking payment and the Clipper card. It's a little more complicated to set up than it should be, but it works. I'll leave the rant up because the meta-point still stands.

This article will look like a random rant about public transportation, but it's really a random rant about bad systems design, especially when extending an existing system without understanding the interactions between the new and the old.

Every day I ride BART (the San Francisco bay area subway system) to and from work. The default procedure for BART is to put some cash or credit into a machine and get a ticket of that value. Then every time you want to ride you run the ticket through a turnstile, ride the train, then run the ticket through an exit turnstile to have value deducted from your ticket based on the distance between the stations. Keep doing that until the ticket is nearly depleted and then add some more value at a ticket machine.

In order to minimize the number of trips to the ticket vending machine I was buying the max value it would allow, $60. But the paper tickets are easily torn, easily lost, and um, easily destroyed in a washing machine. Before you ask yes I did. Fortunately it was a nearly depleted ticket.

Losing $60 on a maxed ticket wouldn't be pleasant. So I decided to get a Clipper card, an RFID based card that you can tie to a prepaid or credit account and use for BART, Muni (San Francisco's municipal bus and light-rail system), and Cal-Train (a commuter rail that connects San Francisco to Silicon Valley). Sounds like a good thing. But in practice it turns out to be (nearly) too much trouble.

I skipped over a point in describing my default procedure with tickets. You see, I have to park at the BART station near my house and BART charges a small $1 fee for parking. So the real procedure is: feed the ticket to the entrance turnstile, use the ticket at another machine to pay for parking, then ride the train, etc. That's important because, get this, you can't use the Clipper card for parking. I have no idea why not, but you can't. Period.

Okay, that's annoying. But I knew that when I got the card so I had a plan: I would continue to buy BART tickets but for much smaller amounts, like $10, in order to pay for parking. It wouldn't be perfect but still much better. Washing away, er accidentally tearing up $10 is much less painful than losing $60.

It was a good plan. But my plan was dashed by some system designer's brain fart. The machine that accepts a parking fee will not accept your ticket unless it has been used at the entrance turnstile. So if I enter using the Clipper card I can't use a ticket for parking.

If you're mentally asking "WTF?" at this point then you and I are on the same page. The designers had to work extra to add this restriction. Maybe it seemed like a good idea before the card system existed: somebody buying parking with a ticket not marked for entrance may have jumped the turnstile. Or something. I don't know. I do know that with the new system it's just a way to render the Clipper card (nearly) too much trouble.

Why too much trouble? Because other than tickets the parking machine only takes cash and only in $1 and $5 denominations. Not credit cards, not debit cards, not twenties. Nope. Only ones and fives. I don't know about you but I often have nothing but twenties because that's what ATMs dispense. Besides, the goal of me getting the Clipper card was to get rid of a bit of paper, not create a need to carry more bits of paper (with pictures of dead white guys).

The card is now very nearly too much trouble. Nearly. I'll still keep the Clipper card. I can use it for my return trips and thus cut the value of my tickets in half. And I can use it for Muni and Cal-Train. But my original goal, getting rid of paper, is dead, slaughtered on the altar of bad systems design.

Now let me get meta. The above sad but true story is an object lesson in extending an existing system. The next time you add a feature to a system you're working on ask yourself if interactions with existing features are going to cause your users problems. At the very least such bad interactions can lower the user's valuation of all your hard work in creating the new feature. But that's not all.

One of the strange things about user experience is that users subjectively put far more weight on negative experiences than positive ones. A new feature should create a positive experience and make users want to use your system even more. But if a new feature interacts poorly with old features then the user's impression can actually be more negative than it was before you extended your system. It would be a damn shame if trying to help your users eventually led them to conclude your whole system was (entirely) too much trouble.

Friday, October 8, 2010

How To Estimate Software

A guide to estimating percentage complete and remaining time.

I haven't looked at the problem. Completed: 0% Time esimate: about 2 weeks.

I've looked at the problem. Completed: 50% Time estimate: about 2 more weeks.

I've implemented almost everything. All that remains is the difficult stuff that I don't really know how to do. Completed: 90% Time estimate: about 2 more weeks.

I've done everything. All that remains is documentation, code review, tests and error handling. Completed: 99% Time estimate: about 2 more weeks.

I still haven't finished documentation, code review, tests, or error handling. The truth is I've been gold plating what I already wrote. But I just saw another problem that seems more interesting. Completed: 100% Time estimate: 0 more weeks.