Недавно я выполнил следующее упражнение, но хотел получить отзывы о том, насколько идиоматична моя программа haskell и т. Д. Это из курса, который знакомит нас с файловым вводом-выводом, прежде чем мы рассмотрим теорию монад, так что мои знания о монадах все еще находятся в зачаточном состоянии. Любая обратная связь очень ценится.
Вот задача:
В
StudentTeacher.hs
обратите внимание, что у нас есть старая версия нашегоPerson
введите сStudent
а такжеTeacher
конструктор. ЗаполнитьstudentTeacherBasic
так что он будет использовать аргументы для определения правильного типа человека. Если аргументы содержат-s
или же--student
прочтите две строчки, имя и возраст, и сделайте учеником. Если там есть-t
или же--teacher
прочтите две строчки для названия и отдела. Если аргументов нет, выведите:
Please specify the type of person!
В противном случае выведите получившееся лицо.
Введите studentTeacherAdvanced, результат должен быть таким же, как у вашей базовой версии, но читать вводимые данные иначе. В качестве первого аргумента возьмите имя файла. Если он заканчивается на .student, прочтите строки из файла, предполагая, что они содержат информацию об ученике. Если это учитель, представьте себе учителя. Выведите то же сообщение об ошибке в любом другом случае.
Вот мой ответ:
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
1 ответ
Перво-наперво: отличная работа по добавлению сигнатуры типа для каждой функции. Теперь давайте посмотрим, как мы можем улучшить код.
Используйте сначала интересный случай, потом другие
Когда мы сопоставим шаблон в привязке или в case
выражения, часто бывает проще начать сначала с интересного случая, а затем обрабатывать Ошибка случаи:
decodeTeacher :: PersonDecoder
decodeTeacher encoded =
case lines of (name:dept:_) -> Just (Teacher name dept)
_ -> Nothing
where lines = splitOn "n" encoded
decodeStudent :: PersonDecoder
decodeStudent encoded =
case lines of (name:age:_) -> decodeStudentLines name age
_ -> Nothing
where lines = splitOn "n" encoded
Проверьте стандартную библиотеку на наличие уже существующей функциональности
Мы остаемся на этих функциях. Название lines
очень прискорбно, так как уже существует lines
в Prelude
. Это функция со следующей подписью:
lines :: String -> [String]
С помощью этой функции мы можем избавиться от splitOn
:
decodeTeacher :: PersonDecoder
decodeTeacher encoded =
case lines encoded of
(name:dept:_) -> Just (Teacher name dept)
_ -> Nothing
decodeStudent :: PersonDecoder
decodeStudent encoded =
case lines encoded of
(name:age:_) -> decodeStudentLines name age
_ -> Nothing
По возможности предпочитайте более простую логику
Далее мы посмотрим на 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
Это довольно длинный if
выражение. С охранником легче читать и обращаться с ним:
parseArgsSuffix :: CLIParser
parseArgsSuffix [] = Nothing
parseArgsSuffix (fname:_)
| fname `hasSuffix` "student" = Just (decodeStudent, openFile fname ReadMode)
| fname `hasSuffix` "teacher" = Just (decodeTeacher, openFile fname ReadMode)
| otherwise = Nothing
Обратите внимание, что мы должны сначала оставить здесь неинтересный случай, так как он улучшает читаемость. Однако мы повторяемся. Что-то вместе
parseArgsSuffix :: CLIParser
parseArgsSuffix [] = Nothing
parseArgsSuffix (fname:_)
| fname `hasSuffix` "student" = decodeVia decodeStudent
| fname `hasSuffix` "teacher" = decodeVia decodeTeacher
| otherwise = Nothing
where
decodeVia f = Just (f, openFile fname ReadMode)
может уменьшить дублирование. Теперь давайте посмотрим на hasSuffix
:
hasSuffix :: String -> String -> Bool
hasSuffix filename suffix = if length components == 0 || length components == 1
then False
else last components == suffix
where
components = splitOn "." filename
Очередной раз, splitOn
здесь перебор. Нам нужно только знать, заканчивается ли наше имя файла заданным расширением. Функция isSuffixOf
из Data.List
справляется с этим:
hasSuffix :: String -> String -> Bool
hasSuffix filename suffix = ('.' : suffix) `isSuffixOf` filename
Возможно, мы могли бы даже удалить hasSuffix
совершенно так.
Убедитесь, что функций полный
parseArgsFlag
частичный. Если мы используем parseArgsFlags
на ["whoops!", "-t"]
, то ни один из шаблонов не соответствует:
parseArgsFlag :: CLIParser
parseArgsFlag [] = Nothing
parseArgsFlag ("-t":_) = Just (decodeTeacher, wrapStdIn)
parseArgsFlag ("-s":_) = Just (decodeStudent, wrapStdIn)
parseArgsFlag ("--teacher":_) = parseArgsFlag ["-t"]
parseArgsFlag ("--student":_) = parseArgsFlag ["-s"]
Если так задумано, тогда все в порядке. Однако, если вы хотите обработать эти случаи, добавьте шаблон match all:
parseArgsFlag _ = Nothing
Кстати, я бы предпочел что-то вроде этого:
parseArgsFlag :: CLIParser
parseArgsFlag args
| any (`elem` args) ["-t", "--teacher"] = Just (decodeTeacher, wrapStdIn)
| any (`elem` args) ["-s", "--student"] = Just (decodeStudent, wrapStdIn)
| otherwise = Nothing