15
\$\begingroup\$

In the book Real World Haskell, the author writes in the first line of every file that uses, for example the following: file: ch05/PrettyJSON. The chapter slash and the name of a module.

I wanted to create a script which reads the first line and copies the files to the appropriate directory. In the above example we should copy the file to the directory ch05. I wanted to write this in Haskell in order to use it for "real world" applications.

But before starting writing to Haskell I wrote it in Python:

import os
import shutil
current_dir = os.getcwd()
current_files=os.listdir(current_dir)
haskell_files=filter(lambda x:x.find(".hs")!=-1,current_files)
for x in haskell_files:
 with open(x,"r") as f:
 header=f.readline()
 try:
 chIndex=header.index("ch")
 except ValueError as e:
 continue
 number=header[chIndex:chIndex+4]
 try:
 os.mkdir(number)
 shutil.copy2(x,number)
except OSError as e:
 shutil.copy2(x,number)

And then I tried to write the same code in Haskell:

f x = dropWhile (\x-> x/='c') x
g x = takeWhile (\x-> x=='c' || x=='h' || isDigit x) x
z= do 
 y<-getCurrentDirectory
 e<-getDirectoryContents y
 let b=findhs e
 mapM w b
findhs filepaths = filter (isSuffixOf ".hs") filepaths
w filename= do
 y<-readFile filename
 let a=(g.f) y
 if "ch" `isPrefixOf` a
 then do
 createDirectoryIfMissing False a
 g<-getCurrentDirectory
 writeFile (g ++"/"++ a ++ "/" ++ filename) y
 else
 return ()
main=z

Haskell code should be more elegant and more compact but it is not. Can you give me suggestions on making the above program more Haskellish?

Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Sep 29, 2012 at 8:26
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Interesting question though. I think it breaks down to: How to code a completely sequential thing in "good" Haskell. \$\endgroup\$ Commented Sep 29, 2012 at 9:00
  • 1
    \$\begingroup\$ You 've got the point @JFritsch. Can we write "good" sequential Haskell programs? This is the real question. Because if we want to do "Real World Haskell" I think that we should know how to write sequential code in Haskell. \$\endgroup\$ Commented Sep 29, 2012 at 9:51
  • \$\begingroup\$ A few links for scripting shell tasks in Haskell: Haskell shell examples, Shelly: Write your shell scripts in Haskell, Practical Haskell Programming: Scripting with Types. \$\endgroup\$ Commented Sep 29, 2012 at 14:04

4 Answers 4

17
\$\begingroup\$

Notes:

  • don't use single letter variable names for top level bindings
  • leave spaces around syntax like = and <-
  • use meaningful variable names for system values (such as file contents)
  • inline single uses of tiny functions, such as your findhs e
  • give your core algorithm, w, a meaningful name
  • don't use long names for local parameters like filepaths. fs is fine.
  • use pointfree style for arguments in the final position
  • use when instead of if .. then ... else return ()
  • use the filepath library for nice file path construction
  • your only use of f and g is to compose them. so do that and name the result.
  • use section syntax for lambdas as function arguments to HOFs
  • indent consistently
  • use mapM_ when the result you don't care about.

Resulting in:

import Data.Char
import Data.List
import Control.Monad
import System.Directory
import System.FilePath
clean = takeWhile (\x -> x == 'c' || x == 'h' || isDigit x)
 . dropWhile (/='c')
process path = do
 y <- readFile path
 let a = clean y
 when ("ch" `isPrefixOf` a) $ do
 createDirectoryIfMissing False a
 g <- getCurrentDirectory
 writeFile (g </> a </> path) y
main = do
 y <- getCurrentDirectory
 e <- getDirectoryContents y
 mapM_ process $ filter (isSuffixOf ".hs") e
answered Sep 29, 2012 at 14:16
\$\endgroup\$
4
  • \$\begingroup\$ much nicer. getCurrentDirectory is superfluous though - in the when clause don't need g, and in main, you can replace y with ".". \$\endgroup\$ Commented Sep 29, 2012 at 15:00
  • 1
    \$\begingroup\$ In the sense of the original python code, the name of target directory has to be computed as take 4 <$> find (isPrefixOf "ch") (tails y) where y is extracted from head . lines <$> readFile path . \$\endgroup\$ Commented Sep 29, 2012 at 15:08
  • 1
    \$\begingroup\$ main can be written as main = getCurrentDirectory >>= getDirectoryContents >>= mapM_ process . filter (isSuffixOf ".hs") \$\endgroup\$ Commented Oct 4, 2012 at 16:03
  • \$\begingroup\$ @nponeccop Much more monadic! \$\endgroup\$ Commented Jul 13, 2015 at 18:24
3
\$\begingroup\$

The first thing you note is that although haskellers like one character variable names in a small scope, we do not like one character functions in a global scope.

There is an implicit partial function in your code. The function that gets the chapter name from the file (called getChapter in the code below), which takes strings (the content of the file) and gives a chapter name if it exists in the string. This seems like a good idea to make explicit using the Maybe type so that this fact does not need to be expressed in the control flow of the program. Note that I have split the getting of the chapter from the contents into the pure part (looking at the string) and the impure part (reading the file), this might be overkill but it's nice to remove impure functions whenever possible.

type Chapter = String
getChapter :: String -> Maybe Chapter
getChapter ('c':'h':xs) = Just ('c':'h': takewhile isDigit xs)
getChapter (_:xs) = getChapter xs
getChapter [] = Nothing
main :: IO ()
main = do 
 dir <- getCurrentDirectory
 files <- getDirectoryContents dir
 mapM_ process (filter (isPrefixOf ".hs") files)
process :: FilePath -> IO ()
process fileName = do
 chapter <- liftM getChapter (readFile fileName)
 when (isJust chapter)
 (copyToChapter fileName (fromJust chapter))
copyToChapter :: FilePath -> Chapter -> IO ()
copyToChapter fileName chapter = do
 createDirectoryIfMissing False chapter
 copyFile fileName (chapter ++ "/" ++ fileName)
answered Sep 29, 2012 at 12:22
\$\endgroup\$
2
  • \$\begingroup\$ Your getChapter is slightly broken, as it takes only the first digit of the chapter. Also, you waste a lot of time reading the whole file, if the chapter is not specified. \$\endgroup\$ Commented Sep 30, 2012 at 11:56
  • \$\begingroup\$ @KarolisJuodelė I fixed the single digit thing. About wasting a lot of time: The original solution does that too (dropWhile (/='c')) so I just thought it was part of the specification. \$\endgroup\$ Commented Sep 30, 2012 at 19:12
1
\$\begingroup\$
getChapter :: String -> IO (Maybe String)
getChapter name = if ".hs" `isSuffixOf` name then do
 ln <- withFile name ReadMode hGetLine
 return $ fmap (take 4) $ find ("ch" `isPrefixOf`) $ tails ln
 -- note, this is what your Pythod code does
 else return Nothing
copyToChapter :: String -> String -> IO ()
copyToChapter file chapter = do
 createDirectoryIfMissing False chapter
 copyFile file (chapter ++ "/" ++ file)
process :: String -> IO ()
process file = do
 chapter <- getChapter file
 maybe (return ()) (copyToChapter file) chapter
main = do 
 dir <- getCurrentDirectory
 files <- getDirectoryContents dir
 mapM process files 

This should be an improvement. Note that this is actually longer. Of course, you could write

getChapter name = if ".hs" `isSuffixOf` name then withFile name ReadMode hGetLine >>= return . fmap (take 4) . find ("ch" `isPrefixOf`) . tails
 else return Nothing
copyToChapter file chapter = createDirectoryIfMissing False chapter >> copyFile file (chapter ++ "/" ++ file)
process file = getChapter file >>= maybe (return ()) (copyToChapter file)
main = getCurrentDirectory >>= getDirectoryContents >>= mapM process

if you like few lines of code. I'd advise against it though.

In general, you should not expect Haskell to be better at procedural tasks than procedural languages.

answered Sep 29, 2012 at 10:01
\$\endgroup\$
1
  • 3
    \$\begingroup\$ hould not expect Haskell to be better at procedural tasks than procedural languages. -- but note that because of monads, structuring procedural programs is a lot easier as code blocks are first class. error handling may also be simplified thanks to monads. Abstraction capaabilities always help. \$\endgroup\$ Commented Sep 29, 2012 at 14:18
1
\$\begingroup\$

Other way you could write your Shell-like scripts in Haskell is using Shelly library. It provides functionality specifically suited for such tasks, kind of DSL, which brings you more brevity. Its GitHub page contains a bunch of usefull links with examples.

E.g. Don's program could be rewritten to:

{-# LANGUAGE OverloadedStrings #-}
import Data.Text.Lazy as LT
import Data.Text.Lazy.IO as LTIO
import Prelude as P hiding (FilePath)
import Data.Char
import Shelly
clean = LT.takeWhile (\x -> x == 'c' || x == 'h' || isDigit x)
 . LT.dropWhile (/='c')
readFileP = liftIO . LTIO.readFile . unpack . toTextIgnore
process path = shelly $ do
 y <- readFileP path
 let a = clean y
 when ("ch" `LT.isPrefixOf` a) $ do
 let p = fromText a
 mkdir_p p
 cp path p
main = shelly $ do
 e <- ls "."
 mapM_ process $ P.filter (hasExt "hs") e

The only flaw here is a bit bloated readFileP. I did not find a better way (which might be due to my beginner level). Possibly such function could become a part of Shelly library itself.

So far I found combination of Haskell and Shelly quite good as "a scripting language for every day task" and start to rewrite my scripts utilizing Shelly. That is a nice entrance gateway to Haskell language itself for me.

answered Sep 29, 2012 at 17:34
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.