gdritter repos documents / master posts / a-small-haskell-record-complaint
master

Tree @master (Download .tar.gz)

a-small-haskell-record-complaint @masterraw · history · blame

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.