Record syntax default value for accessor

2019-02-16 18:19发布

问题:

As I was writing up an answer just now, I ran across an interesting problem:

data Gender = Male | Female
            deriving (Eq, Show)

data Age = Baby | Child | PreTeen | Adult
         deriving (Eq, Show, Ord)

data Clothing = Pants Gender Age
              | Shirt Gender Age
              | Skirt Age         -- assumed to be Female
              deriving (Show, Eq)

Suppose I wish to write the final data type with record syntax:

data Clothing = Pants {gender :: Gender, age :: Age}
              | Shirt {gender :: Gender, age :: Age}
              | Skirt {age :: Age}
              deriving (Show, Eq)

The problem is, I want gender $ Skirt foo to always evaluate to Female (regardless of foo, which is an Age). I can think of a few ways to accomplish this, but they require that I either

  1. use smart constructors, theoretically allowing Skirt Male foo but not exposing Constructors
  2. define my own gender function

With #1, by not exposing the constructor in the module, I effectively prevent users of the module from taking advantage of record syntax. With #2, I have to forego record syntax entirely, or define an additional function gender', which again defeats record syntax.

Is there a way to both take advantage of record syntax, and also provide a "default", unchangeable value for one of my constructors? I am open to non-record-syntax solutions as well (lenses, perhaps?) as long as they are just as elegant (or moreso).

回答1:

Is there a way to both take advantage of record syntax, and also provide a "default", unchangeable value for one of my constructors?

In the absence of a convincing counterexample, the answer seems to be "no".



回答2:

Yes there is a tension between types and data... which by the way shows how thin is the line.

The pratical answer is to use a default instance as indicated in the Haskell Wiki. It does answer your exact question since you must give up direct constructor use.

Thus for your example,

data Age = Baby | Child | PreTeen | Adult | NoAge
data Clothing = Pants {gender :: Gender, age :: Age}
              | Shirt {gender :: Gender, age :: Age}
              | Skirt {gender :: Gender, age :: Age}
              deriving (Show, Eq)

skirt = Skirt { gender=Female, age=NoAge }

then developpers can create new instances with default values, using the copy-and-update facility of the record syntax

newSkirt = skirt { age=Adult }

and gender newSkirt evaluates to Female

I want to stress that this approach leads you to define default values at the type level, which I think is a Good Thing (of course the NoAge constructor is the Nothing of a Maybe Age type).