Explanation
I started learning Haskell about a month ago. As an exercise, I recreated a little command line tool, which I had previously written in PowerShell. In the current state, it displays a list of videos in a specific directory and its subdirectories. Later I will add the functionality to play or delete those videos (which should be easy). But the big part is displaying the list. That's all this code does, at the moment.
As much as I like Haskell so far, I'm unhappy with how wordy and complicated this code is. It is 100 lines and seems very hard to read. The PowerShell script is only 70 lines (complete with playback and deletion of videos), less wordy and very easy to read.
Is it mostly due to my lack of knowledge how to write good Haskell code? Or is Haskell not a good tool for this kind of task?
What bothers me in particular:
I have all those little functions, which do one specific task and call each other to achieve the end goal. This makes it very hard to read the code if one is not familiar with it. Because the functions are not connected in an obvious control structure, one has to analyse each one, see what other function it calls and so forth until the picture is complete.
Compare this with an imperative language: there are someif
statements with easy to read conditions and some large code blocks. At a glance one can see what is done where and if it's of interest, one can look more into the details of the implementation. It's very easy to get a feeling of the overall structure of the program.getRecursiveContents
(I copied this from the internet). It's huge and complicated. Getting files recursively is such an everyday task - is there really no library function which does this?It's nice that I can describe how a custom type should
Show
itself when printed. But because I'm dealing with lists, I then have tounlines $ map show
it, which is not pretty.
Directory structure
└───Videos
│ Heat.1995.1080p.BRrip.x264.YIFY.mp4
│ heat.png
│ leon.png
│ Leon.the.Professional.Extended.1994.BrRip.x264.YIFY.mp4
│ mononoke hime.png
│ Mononoke.hime.[Princess.Mononoke].[DUAL.AUDIO]1997.HDTVRip.x264.YIFY.mkv
│ Oblivion.2013.1080p.BluRay.x264.YIFY.mp4
│ oblivion.png
│ terminator 2.png
│ Terminator.2.Judgment.Day.1991.DC.1080p.BRrip.x264.GAZ.YIFY.mp4
│ traffic.png
│
└───Series
S01E01.Some.Show.mp4
S01E02.Some.Show.mp4
S01E03.Some.Show.mp4
Output
Videos
1 Heat.1995.1080p.BRrip.x264.YIFY
2 Leon.the.Professional.Extended.1994.BrRip.x264.YIFY
3 Mononoke.hime.[Princess.Mononoke].[DUAL.AUDIO]1997.HDTVRip.x264.YIFY
4 Oblivion.2013.1080p.BluRay.x264.YIFY
5 Terminator.2.Judgment.Day.1991.DC.1080p.BRrip.x264.GAZ.YIFY
Series
6 S01E01.Some.Show
7 S01E02.Some.Show
8 S01E03.Some.Show
Source
module Main where
import Control.Monad (forM)
import Data.Char (toLower)
import Data.List (isInfixOf, nub, sort, sortBy)
import Data.List.Split (splitOn)
import System.Directory (doesDirectoryExist, listDirectory)
import System.FilePath (takeBaseName, takeDirectory, takeExtension, (</>))
import Text.Printf (printf)
videoDirectory = "C:\\Users\\Swonkie\\Downloads\\Videos"
videoExtensions = [".mp4", ".mkv", ".avi", ".m4v"]
-- ANSI / VT color codes
color = "\ESC[1;31m"
reset = "\ESC[m"
type Library = [Directory]
data Directory = Directory { name :: String
, files :: [Video]
}
instance Show Directory where
show (Directory name files) = " " ++ color ++ name ++ reset ++ "\n" ++ (unlines $ map show files)
data Video = Video { index :: Integer
, path :: FilePath
}
instance Show Video where
show (Video i path) = printf "%3d %s" i (takeBaseName path)
isVideoFile :: FilePath -> Bool
isVideoFile path = takeExtension path `elem` videoExtensions
-- | not used yet
getVideoByIndex :: [Video] -> Integer -> Maybe Video
getVideoByIndex files i =
if length v > 0
then Just (head v)
else Nothing
where v = filter (\ v -> index v == i) files
-- | not used yet
getVideoByName :: [Video] -> String -> Maybe Video
getVideoByName files s =
if length v > 0
then Just (head v)
else Nothing
where v = filter (\ v -> isInfixOf (map toLower s) (map toLower $ takeBaseName $ path v)) files
-- | The name of the folder containing the file, without its parent folders.
bottomFolder :: FilePath -> String
bottomFolder path = last $ splitOn "\\" $ takeDirectory path
-- | A list of all unique directory names which appear in the list of videos.
getDirectories :: [Video] -> [String]
getDirectories videos = nub $ map (bottomFolder . path) videos
-- | Filters the list of videos down to only those which are in a specific directory.
getVideosInDirectory :: [Video] -> String -> [Video]
getVideosInDirectory videos name = filter (\ v -> (bottomFolder $ path v) == name) videos
-- | Bundles the videos in a specific directory in a Directory type.
getDirectory :: [Video] -> String -> Directory
getDirectory videos name = Directory name (getVideosInDirectory videos name)
-- | Creates Video objects with indexes
getVideos :: [FilePath] -> [Video]
getVideos list = [Video (fst tp) (snd tp) | tp <- zip [1..] list]
-- | Gets all the directories of the videos and creates a list of Directory types.
getLibrary :: [Video] -> Library
getLibrary videos = map (getDirectory videos) $ getDirectories videos
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topdir = do
names <- listDirectory topdir
paths <- forM names $ \ name -> do
let path = topdir </> name
isDirectory <- doesDirectoryExist path
if isDirectory
then getRecursiveContents path
else return [path]
return (concat paths)
main :: IO ()
main = do
-- get all video files recursively
files <- getRecursiveContents videoDirectory
let videoFiles = sort $ filter isVideoFile files
-- adding a character to the end of the path is a hack, to have subdirs sorted below parent dirs
-- apparently "end of string" is last in the sort order, not first (weird)
let sortedByDirectory = sortBy (\ a b -> compare (takeDirectory a ++ "$") (takeDirectory b ++ "$")) videoFiles
let lib = getLibrary $ getVideos sortedByDirectory
-- show the list of videos
putStrLn ""
putStr $ unlines $ map show lib