पाठ 7 / 25

Algebraic Data Types and Records

Model data with sum types, product types, records and newtypes.

Designing with types

Algebraic data types are declared with data. A product type combines values: data Item = Item String Int holds a SKU and a quantity. A sum type offers alternatives: data Payment = Card String | Upi String | Cash is one of these constructors. Most real types mix both, and constructors can be parameterised by types: data Tree a = Leaf | Node (Tree a) a (Tree a). Record syntax names fields and generates accessor functions: data Customer = Customer { name :: String, tier :: Tier }, with record update c { tier = Gold } producing a modified copy. newtype wraps exactly one value in a distinct type at zero runtime cost, such as newtype Paise = Paise Int, preventing confusion between paise and rupees or customer and order IDs. type creates only an alias. Use deriving (Show, Eq, Ord) to generate common instances. Precise types make illegal states unrepresentable: rather than a status string plus an optional tracking number, use Shipped TrackingId, which can only exist with a tracking number.

Sum and product types

A product holds several values together; a sum is one of several alternatives.

Left: a single box divided into labelled compartments. Right: a fork in a path leading to three differently shaped boxes.
Figure 3.1 — Product types and sum types.

Modelling orders with ADTs

Sum types, records, newtypes and exhaustive functions.

newtype Paise = Paise Int deriving (Show, Eq, Ord)
newtype TrackingId = TrackingId String deriving (Show, Eq)

data Payment
  = Card { last4 :: String }
  | Upi String
  | Cash
  deriving (Show, Eq)

data OrderState
  = Placed
  | Paid Payment
  | Shipped Payment TrackingId
  | Cancelled String
  deriving (Show, Eq)

data Order = Order
  { orderId :: String
  , total   :: Paise
  , state   :: OrderState
  } deriving (Show, Eq)

describePayment :: Payment -> String
describePayment (Card digits) = "card ending " ++ digits
describePayment (Upi vpa)     = "UPI " ++ vpa
describePayment Cash          = "cash on delivery"

ship :: TrackingId -> Order -> Either String Order
ship tid order = case state order of
  Paid p -> Right order { state = Shipped p tid }          -- record update
  other  -> Left ("cannot ship an order that is " ++ show other)

main :: IO ()
main = do
  let o = Order { orderId = "o-1", total = Paise 49900, state = Paid (Upi "asha@okbank") }
  print (ship (TrackingId "TRK123") o)
  print (ship (TrackingId "TRK124") o { state = Placed })
  putStrLn (describePayment (Card "4242"))

Avoid partial record fields

In Payment, last4 exists only for Card, so last4 Cash crashes at run time. Prefer record syntax only for single-constructor types, or match on constructors; GHC can warn with -Wpartial-fields.

त्वरित जाँच: What does newtype Paise = Paise Int give you?

  • A distinct type wrapping Int with no runtime cost
  • A type alias with no checking
  • A mutable integer
  • A list of integers
Answer

A distinct type wrapping Int with no runtime cost — newtype creates a new type for the compiler that is erased at run time.