I recently completed the following exercise, but wanted some feedback on how idiomatic etc. my haskell program is. This is from a course which introduces us to file IO before we cover the theory of monads, so my monad-knowledge is still in its infancy. Any feedback much appreciated.
Here's the task:
In
StudentTeacher.hs
, observe that we have an old version of ourPerson
type with theStudent
andTeacher
constructor. Fill instudentTeacherBasic
so that it will use arguments to determine the right type of person. If the arguments contain-s
or--student
, read two lines, for name and age, and make a student. If there is-t
or--teacher
, read two lines for the name and department. If there are no arguments, print:
Please specify the type of person!
Print the resulting person otherwise.
Fill in studentTeacherAdvanced, which should have the same result as your basic version, but read its input differently. Take a file name as the first argument. If it ends in .student, read lines from the file assuming they contain a student's information. If it’s .teacher, assume a teacher. Print the same failure message in any other case.
Here's my answer:
module StudentTeacher where
import Data.List.Split (splitOn)
import Text.Read (readMaybe)
import System.IO (openFile, withFile, IOMode(..), hGetLine, hGetContents, Handle, stdin)
import System.Environment (getArgs)
import Data.Maybe (isNothing)
data Person =
Student String Int |
Teacher String String
deriving (Show)
type PersonDecoder = String -> Maybe Person
type CLIParser = [String] -> Maybe (PersonDecoder, (IO Handle))
studentTeacher :: CLIParser -> IO ()
studentTeacher cliparser = do
arguments <- getArgs
maybePerson <- case (cliparser arguments) of
Nothing -> return Nothing
Just (decoder, getHandle) -> do
handle <- getHandle
decodePersonFromHandle decoder handle
case maybePerson of
Nothing -> putStrLn "Please specify the type of person"
Just person -> print person
studentTeacherBasic :: IO ()
studentTeacherBasic = studentTeacher parseArgsFlag
decodePersonFromHandle :: PersonDecoder -> Handle -> IO (Maybe Person)
decodePersonFromHandle decoder handle = do
line1 <- hGetLine handle
line2 <- hGetLine handle
return $ decoder $ line1 ++ "\n" ++ line2
parseArgsFlag :: CLIParser
parseArgsFlag [] = Nothing
parseArgsFlag ("-t":_) = Just (decodeTeacher, wrapStdIn)
parseArgsFlag ("-s":_) = Just (decodeStudent, wrapStdIn)
parseArgsFlag ("--teacher":_) = parseArgsFlag ["-t"]
parseArgsFlag ("--student":_) = parseArgsFlag ["-s"]
wrapStdIn :: IO Handle
wrapStdIn = do
return stdin
decodeTeacher :: PersonDecoder
decodeTeacher encoded =
case lines of [] -> Nothing
[_] -> Nothing
(name:dept:_) -> Just (Teacher name dept)
where lines = splitOn "\n" encoded
decodeStudent :: PersonDecoder
decodeStudent encoded =
case lines of [] -> Nothing
[_] -> Nothing
(name:age:_) -> decodeStudentLines name age
where lines = splitOn "\n" encoded
decodeStudentLines :: String -> String -> Maybe Person
decodeStudentLines name age =
case decodedAge of Nothing -> Nothing
(Just ageInt) -> Just (Student name ageInt)
where decodedAge = readMaybe age :: Maybe Int
studentTeacherAdvanced :: IO ()
studentTeacherAdvanced = studentTeacher parseArgsSuffix
parseArgsSuffix :: CLIParser
parseArgsSuffix [] = Nothing
parseArgsSuffix (fname:_) =
if hasSuffix fname "student" then Just (decodeStudent, openFile fname ReadMode)
else if hasSuffix fname "teacher" then Just (decodeTeacher, openFile fname ReadMode)
else Nothing
hasSuffix :: String -> String -> Bool
hasSuffix filename suffix = if length components == 0 || length components == 1
then False
else last components == suffix
where
components = splitOn "." filename