पाठ 16 / 25

Modules, Packages, Text and Containers

Organise code into modules and use Text, Map and Set efficiently.

Structuring programs

Each file is a module (module Shop.Pricing (withGst, Paise (..)) where), whose export list controls the public API; hiding constructors behind smart constructors enforces invariants. Imports can be qualified to avoid name clashes, the standard practice for containers: import qualified Data.Map.Strict as Map. Key libraries beyond base: text (Data.Text, efficient Unicode strings; enable OverloadedStrings so string literals can be Text), bytestring for binary data, containers (Data.Map, Data.Set, Data.Sequence, Data.IntMap), unordered-containers (hash maps), vector (arrays), time, aeson (JSON) and mtl/transformers. Use String only for simple scripts; Text is much faster and more memory-efficient. Declare dependencies with version bounds in the .cabal file (containers >= 0.6 && < 0.8) so builds remain reproducible, and add a cabal.project.freeze file or use Stack snapshots for applications. Organise larger applications into a library with most code and a thin executable, which also makes testing easier.

A Cabal package

A library holds most modules; executables and tests depend on it.

A package box containing a large library block with module tiles, connected to a small executable block and a test block.
Figure 6.1 — Library, executable and test suite.

Summarising a CSV with Text and Map

Qualified imports, OverloadedStrings and fromListWith.

{-# LANGUAGE OverloadedStrings #-}
module Main (main) where

import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Text.Read (readMaybe)

parseRow :: Text -> Maybe (Text, Int)
parseRow line = case T.splitOn "," line of
  [_orderId, city, amount] -> (,) (T.strip city) <$> readMaybe (T.unpack (T.strip amount))
  _                        -> Nothing

revenueByCity :: [Text] -> Map.Map Text Int
revenueByCity rows = Map.fromListWith (+) [r | Just r <- map parseRow rows]

main :: IO ()
main = do
  contents <- TIO.readFile "orders.csv"
  let rows = drop 1 (T.lines contents)          -- skip the header
      totals = revenueByCity rows
  mapM_ (\(c, t) -> TIO.putStrLn (c <> ": " <> T.pack (show t))) (Map.toList totals)
  TIO.putStrLn ("cities: " <> T.intercalate ", " (Map.keys totals))

Qualify container imports

Data.Map exports filter, map, lookup and null, which clash with the Prelude. Importing it qualified (Map.lookup) is the community convention and keeps code readable.

त्वरित जाँच: Why prefer Data.Text over String in real applications?

  • String does not support Unicode
  • Text is required by GHC
  • Text is a compact, efficient representation, while String is a linked list of characters
  • String cannot be printed
Answer

Text is a compact, efficient representation, while String is a linked list of characters — String is [Char], which is slow and memory-hungry for large text.