Librarian of Alexandria

2013-09-17

A Petty And Insignificant Complaint About Haskell Records

Haskell records have lots of problems. Here's one that came up for me today.

You are allowed to export record members without exporting the constructor, for example, if you want to ensure some property is true of the constructed values. In the following example, the field isNeg is effectively a function of the field num:

module Foo(mkRec, num, isNeg) where

data Rec = Rec
  { num   :: Int
  , isNeg :: Bool
  }

mkRec :: Int -> Rec
mkRec n = Rec n (n < 0)

Another module can't use the Rec constructor, but can observe the values using the exported accessors

module Bar where

addRecs :: Rec -> Rec -> Rec
addRecs r1 r2 = mkRec (num r1 + num r2)

Unfortunately, there's a hole here, which is that exporing the accessors allows us to use record update syntax, which means that we can now construct arbitrary values:

constructAnyRec :: Int -> Bool -> Rec
constructAnyRec n b = mkRec 0 { num = n, isNeg = b }

There is a way around this, namely, by rewriting the original module with manual accessors for num and isNeg:

module Foo2(mkRec, num, isNeg) where

data Rec = Rec
  { _num   :: Int
  , _isNeg :: Bool
  }

num :: Rec -> Int
num = _num

isNeg :: Rec -> Bool
isNeg = _isNeg

mkRec :: Int -> Rec
mkRec n = Rec n (n < 0)

However, I'd assert that, morally, the correct thing to do would be to disallow record update at all if the constructor is not in-scope. The purpose of hiding the constructor at all is to ensure that a programmer must perform certain computations in order to construct a valid value, e.g. to enforce invariants on constructed data (as I'm doing here), or to avoid the possibility of pattern-matching on data. If you a programmer hides a constructor but exports its accessors, then generally I'd assert it's because of the former reason, so it would be sensible to prevent record update, as you could always write your own updates, if you so desire.

Of course, pointing out this flaw in light of the other problems with the Haskell record system is like complaining about the in-flight movie on a crashing plane, but still.