The idea is pretty simple, get a list of files in a directory (ignoring subdirectories), and generate of a list of links to those files. Given a directory containing images, for example ‘a.png’, ‘b.png’, and ‘c.png’, we want to output:
<a href="a.png">a.png</a><br /> <a href="b.png">b.png</a><br /> <a href="c.png">c.png</a>
I first wrote it in Clojure which was pretty straight forward:
(ns clojure-directory-lister.core (:require [clojure.string :as s])) (defn files [dir-str] (file-seq (clojure.java.io/file dir-str))) (defn names [files] (map #(.getName %) files)) (defn to-html [filenames] (s/join "<br />" (map #(format "<a href=\"%s\">%s</a>" % %) filenames))) (defn -main [dir out] (spit out (-> dir files names to-html)))
This works fine, but I thought why spend precious seconds waiting for the JVM to start, when I could take hours and write it in Haskell and have it run instantaneously.
Writing it in Haskell follows the same logic and was also easy to write once I figured out how deal with the monadic IO. While Clojure has
file-seq which returns a lazy-seq of all the files in the directory as Java File objects, Haskell’s getDirectoryContents has the return type of
IO [String]. This is a list (in the IO monad) of strings representing both filepaths and directories, and you need to use a seperate function doesFileExist to select only files and not directories. This was tricker than it seems. My first instict
was to write this as:
filter doesFileExist $ getDirectoryContents path
However, this doesn’t work for a couple of reasons. To start, filter expects a function of type
a -> Bool, and since
doesFileExist needs to hit the filesystem, it returns
IO Bool. Luckily there’s a version of filter in Control.Monad called filterM, which works with predicates that wrap their result in a monad, which leads us to:
filterM doesFileExist $ getDirectoryContents path
However this still doesn’t work because filterM expects a plain list of type
[a], and getDirectoryContents returns a list wrapped in the IO monad. To solve this problem, we can use the bind operator
>>= to unwrap the result of getDirectoryContents and apply filterM to it.
getDirectoryContents path >>= filterM doesFileExist
From there the rest of the program is simple:
import System.Directory (getDirectoryContents, doesFileExist) import Control.Monad (filterM) import Text.Printf (printf) import Data.List (intercalate, sort) import System.Environment (getArgs, getProgName) getFiles :: FilePath -> IO [FilePath] getFiles path = getDirectoryContents path >>= filterM doesFileExist fileToLink :: FilePath -> String fileToLink path = printf "<a href=\"%s\">%s</a>" path path main = do progName <- getProgName args <- getArgs if length args /= 2 then putStrLn $ printf "Usage: %s <directory> <outfile>" progName else do let (dirpath:outfile:_) = args unsorted_files <- getFiles dirpath let files = sort unsorted_files let links = map fileToLink files writeFile outfile $ intercalate "<br />\n" links
The only other tricky thing I ran into was the fact that using an if expression inside a do block requires special indentation where ‘then’ and ‘else’ need to be indented farther than the ‘if’. Apparently there’s a compiler extension that will allow you to indent ifs in do blocks normally, but it didn’t seem worth enabling an extention for something this simple.
The full cabal project is on github here: https://github.com/cgag/directory-lister