content: 
  {-# Language BangPatterns #-}
  {-# Language DeriveAnyClass #-}
  {-# Language DerivingStrategies #-}
  {-# Language DuplicateRecordFields #-}
  {-# LANGUAGE DeriveGeneric #-}
  {-# Language GeneralizedNewtypeDeriving #-}
  {-# Language LambdaCase #-}
  {-# Language ParallelListComp #-}
  
  import System.Environment (getArgs)
  import Data.List (foldl', maximumBy, permutations, sortBy)
  import Data.Function
  import Data.Map.Strict (Map)
  import qualified Data.Map.Strict as M
  import Control.DeepSeq
  import GHC.Generics (Generic)
  
  import GHC.Debug.Stub
  
  import Debug.Trace
  
  data MovieDB = MovieDB
      { users   :: !(Map UserID User)
      , movies  :: !(Map MovieID Movie)
      , ratings :: ![Rating]
      }
      deriving stock (Generic)
      deriving anyclass (NFData)
  
  newtype UserID = UserID Int
      deriving newtype (Eq,Ord,Num,Enum)
      deriving stock (Generic)
      deriving anyclass (NFData)
  
  data User = User
      { name  :: !String
      , trust :: !Double -- ^ How trusted this user is. In range [0,1]. Higher is more trustworthy.
      }
      deriving stock (Generic)
      deriving anyclass (NFData)
  
  newtype MovieID = MovieID Int
      deriving newtype (Eq,Ord,Num,Enum)
      deriving stock (Generic, Show)
      deriving anyclass (NFData)
  
  type Movie = String
  
  data Rating = Rating
      { user   :: !UserID   -- ^ Who rated this movie.
      , movie  :: !MovieID  -- ^ Movie being rated.
      , rating :: !Double   -- ^ Rating In range [1,10]. Higher is better.
      }
      deriving stock (Generic)
      deriving anyclass (NFData)
  
  main :: IO ()
  main = withGhcDebug $ do
      -- Parse DB size from arguments
      [nUsersStr, nMoviesStr, nRatingsStr] <- getArgs
      let
          nUsers   = read nUsersStr
          nMovies  = read nMoviesStr
          nRatings = read nRatingsStr
  
      -- Create the DB
      putStrLn $ "Creating the DB with size ("
                  ++ show nUsers ++ " users,"
                  ++ show nMovies ++ " movies,"
                  ++ show nRatings ++ " ratings)"
      let !movieDB = let
              users = M.fromList
                  $ zip
                      [0..]
                      [ User name trust
                      | name <- take nUsers
                          $ fmap unwords
                          $ cycle
                          $ permutations ["abcdefghijklmnopqrstuvwxyz"]
                      | trust <- cycle [0.0, 0.1 .. 1.0]
                      ]
  
              movies = M.fromList
                  $ zip
                      [0..]
                      (take nMovies
                          $ fmap unwords
                          $ cycle
                          $ permutations ["The", "Cat", "In", "The", "Hat", "House", "Mouse", "Story", "Legend"]
                      )
  
              ratings = take nRatings
                  $ cycle
                  $ zipWith3
                      Rating
                      (cycle (M.keys users))
                      (cycle (M.keys movies))
                      (cycle [1.0, 1.01 .. 9])
  
              in force $ MovieDB
                  { users = users
                  , movies = movies
                  , ratings = ratings
                  }
  
      do
          let msg = "DB created."
          traceEventIO msg
          putStrLn msg
      putStrLn "Hit ENTER to continue."
      _ <- getLine
  
  
      -- A single user's favorite movie.
      do
          let
              (userIDA, userA) = M.assocs (users movieDB) !! 0
              favoriteMovieID
                  = snd
                  $ maximumBy (compare `on` fst)
                      [ (rating, movieID)
                      | Rating userIDB movieID rating <- ratings movieDB
                      , userIDA == userIDB
                      ]
  
          putStrLn $ "User '" ++ name userA ++ "''s favorite movie: " ++ (movies movieDB M.! favoriteMovieID)
  
      -- Top rated movies
      -- each rating is weighted by the user's trust score.
      do
          let
              weight_movies = foldl' go M.empty (ratings movieDB)
  
              go :: Map MovieID (Double, Double) -> Rating -> Map MovieID (Double, Double)
              go weights (Rating userID movieID score) = M.alter
                  (\case
                      Nothing -> Just (userTrust * score, userTrust)
                      Just (weightedScore, weight) -> let
                          weightedScore' = weightedScore + (userTrust * score)
                          weight'        = weight        + userTrust
                          in Just (weightedScore', weight')
                  )
                  movieID
                  weights
                  where
                  userTrust = trust ((users movieDB) M.! userID)
  
          saveClosures [Box weight_movies]
          putStrLn "Pausing"
          getLine
  
              -- (Movie ID, rating) sorted by rating.
          let movieRatings :: [(MovieID, Double)]
              movieRatings
                  = sortBy (compare `on` snd)
                  $ fmap (\(movieID, (weightedRating, weight)) -> (movieID, weightedRating / weight))
                  $ filter (\(_, (_, weight)) -> weight /= 0)
                  $ M.assocs weight_movies
  
          putStrLn "Top 10 movies:"
          putStrLn $ unlines $ fmap show $ take 10 $ reverse $ movieRatings
  
      traceEventIO "Top 10 movies done."
      putStrLn "Hit ENTER to continue."
      _ <- getLine
  
      touch movieDB
  
  {-# NOINLINE touch #-}
  touch :: a -> IO ()
  touch a = return ()
  