Программа на Haskell с базовым синтаксическим анализом CLI, чтением файлов и декодированием содержимого файлов в указанные типы

Недавно я выполнил следующее упражнение, но хотел получить отзывы о том, насколько идиоматична моя программа 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 ответ
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

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *