JSON, Files and Command-Line Apps

28.1 JSON with aeson

{-# LANGUAGE DeriveGeneric, DeriveAnyClass, OverloadedStrings #-}
import Data.Aeson
import GHC.Generics (Generic)
import qualified Data.ByteString.Lazy.Char8 as BL

data Person = Person
  { name :: String
  , age :: Int
  , emails :: [String]
  }
  deriving (Show, Generic, ToJSON, FromJSON)      -- automatic instances via Generics

main :: IO ()
main = do
  let ana = Person "Ana" 30 ["ana@example.com"]
  BL.putStrLn (encode ana)                        -- {"name":"Ana","age":30,"emails":["ana@example.com"]}
  print (decode "{\"name\":\"Bo\",\"age\":25,\"emails\":[]}" :: Maybe Person)
  print (eitherDecode "{\"name\":1}" :: Either String Person)   -- a descriptive error

-- Manual instances for custom field names / validation
data Item = Item {itemId :: Int, title :: String} deriving Show
instance FromJSON Item where
  parseJSON = withObject "Item" $ \o -> Item <$> o .: "id" <*> o .: "title"
instance ToJSON Item where
  toJSON (Item i t) = object ["id" .= i, "title" .= t]

28.2 Command-Line Parsing with optparse-applicative

Subcommands (hsubparser), environment variables, and automatic --help. Other useful CLI libraries: turtle (shell scripting), typed-process (run external processes), ansi-terminal, brick (terminal UIs), filepath/directory.

import Options.Applicative

data Options = Options
  { optInput :: FilePath
  , optCount :: Int
  , optVerbose :: Bool
  }

optionsP :: Parser Options
optionsP =
  Options
    <$> strArgument (metavar "FILE" <> help "Input file")
    <*> option auto (long "count" <> short 'n' <> value 10 <> showDefault <> help "Number of lines")
    <*> switch (long "verbose" <> short 'v' <> help "Verbose output")

main :: IO ()
main = do
  opts <- execParser (info (optionsP <**> helper) (fullDesc <> progDesc "Show the head of a file"))
  contents <- readFile (optInput opts)
  mapM_ putStrLn (take (optCount opts) (lines contents))