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 ourPersontype with theStudentandTeacherconstructor. Fill instudentTeacherBasicso that it will use arguments to determine the right type of person. If the arguments contain-sor--student, read two lines, for name and age, and make a student. If there is-tor--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