System.IO — 파일·핸들·입출력
System.IO — 파일·핸들·입출력
System.IO는 파일과 표준 입출력 장치를 다루는 핵심 모듈이에요. IO 모나드 자체, 파일을 열고 닫는 핸들(Handle) 관리, 버퍼링, 파일 위치 이동, 텍스트 입력·출력에 이르기까지 Haskell 프로그램의 입출력 대부분이 이 모듈을 통해 이뤄져요. Prelude가 내보내는 기본 입출력 함수들도 대부분 여기서 정의돼요.
본문
이 모듈이 내보내는 것들은 다음과 같아요.
module System.IO (
IO, fixIO, FilePath, Handle, stdin, stdout, stderr, withFile,
openFile, IOMode(ReadMode, WriteMode, AppendMode, ReadWriteMode), hClose,
readFile, writeFile, appendFile, hFileSize, hSetFileSize, hIsEOF,
isEOF, BufferMode(NoBuffering, LineBuffering, BlockBuffering),
hSetBuffering, hGetBuffering, hFlush, hGetPosn, hSetPosn, HandlePosn,
hSeek, SeekMode(AbsoluteSeek, RelativeSeek, SeekFromEnd), hTell,
hIsOpen, hIsClosed, hIsReadable, hIsWritable, hIsSeekable,
hIsTerminalDevice, hSetEcho, hGetEcho, hShow, hWaitForInput, hReady,
hGetChar, hGetLine, hLookAhead, hGetContents, hPutChar, hPutStr,
hPutStrLn, hPrint, interact, putChar, putStr, putStrLn, print,
getChar, getLine, getContents, readIO, readLn
) where
41.1 IO 모나드
data IO a
IO a 타입의 값은, 수행되면 타입 a의 값을 반환하기 전에 어떤 I/O를 수행하는 계산이에요. I/O 동작을 '수행'하는 방법은 정말 하나뿐이에요: 프로그램에서 Main.main에 바인딩하는 것. 프로그램이 실행되면 그 I/O가 수행돼요. 임의의 함수에서 I/O를 수행하는 것은 불가능해요. 그 함수 자체가 IO 모나드 안에 있고 Main.main에서 직간접적으로 호출되는 경우가 아니라면 말이죠. IO는 모나드이므로, IO 동작들은 do-표기법이나 Monad 클래스의 >>·>>= 연산을 사용해 결합할 수 있어요.
instance Monad IO instance Functor IO
fixIO :: (a -> IO a) -> IO a
41.2 파일과 핸들
type FilePath = String
파일·디렉터리 이름은 String 타입의 값이며, 그 정확한 의미는 운영체제에 따라 달라져요. 파일을 열면 핸들을 얻는데, 그 핸들로 그 파일의 내용을 조작할 수 있어요.
data Handle
Haskell은 Handle 타입의 값으로 표현되는 파일에 문자를 읽고 쓰는 연산을 정의해요. 이 타입의 각 값은 핸들(handle)인데, Haskell 런타임 시스템이 파일 시스템 객체와의 I/O를 관리하기 위해 사용하는 레코드예요. 핸들은 적어도 다음 속성들을 가져요: 입력·출력 또는 둘 다를 관리하는지; 열려 있거나(open), 닫혀 있거나(closed), 반쯤 닫혀 있는지(semi-closed); 객체가 탐색(seek) 가능한지; 버퍼링이 비활성화됐거나 라인·블록 단위로 활성화됐는지; 버퍼(길이가 0일 수도 있음). 대부분의 핸들은 다음 입력·출력 연산이 일어날 위치를 나타내는 현재 I/O 위치도 가져요. 핸들이 입력만 또는 입력·출력 둘 다를 관리한다면 '읽기 가능(readable)'하고, 그와 비슷하게 출력만 또는 둘 다를 관리한다면 '쓰기 가능(writable)'해요. 핸들은 처음 할당될 때 열려 있어요. 일단 닫히면 입력·출력 어느 쪽에도 더 이상 사용할 수 없지만, 참조가 남아 있는 동안 구현이 그 저장 공간을 재사용할 수는 없어요. 핸들은 Show와 Eq 클래스에 있어요. 핸들을 표시해서 만들어지는 문자열은 시스템에 따라 달라지며, 디버깅을 위해 핸들을 식별할 수 있을 만큼의 정보를 담아야 해요. 핸들은 ==에 따르면 자기 자신에게만 같고, 서로 다른 핸들의 내부 상태를 비교하려는 시도는 하지 않아요.
instance Eq Handle instance Show Handle
41.2.1 표준 핸들
세 개의 핸들이 프로그램 초기화 중에 할당되며, 처음에는 열려 있어요.
stdin :: Handle
Haskell 프로그램의 표준 입력 채널에서 입력을 관리하는 핸들이에요.
stdout :: Handle
Haskell 프로그램의 표준 출력 채널로 출력을 관리하는 핸들이에요.
stderr :: Handle
Haskell 프로그램의 표준 오류 채널로 출력을 관리하는 핸들이에요.
41.3 파일 열기와 닫기
41.3.1 파일 열기
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile name mode act는 openFile을 사용해 파일을 열고 결과 핸들을 계산 act에 전달해요. 핸들은 정상 종료든 예외를 던지든 withFile에서 빠져나갈 때 닫혀요. 핸들을 닫는 과정에서 예외가 발생하면, act가 던진 예외 대신 그 예외가 withFile에 의해 던져져요.
openFile :: FilePath -> IOMode -> IO Handle
계산 openFile file mode는 파일 file을 관리하는 새로 열린 핸들을 할당하고 돌려줘요. mode가 ReadMode이면 입력을, WriteMode나 AppendMode이면 출력을, ReadWriteMode이면 입력·출력 둘 다를 관리해요. 파일이 존재하지 않고 출력용으로 열린다면 새 파일로 만들어져야 해요. mode가 WriteMode이고 파일이 이미 존재한다면 길이 0으로 잘라내야 해요. 어떤 운영체제는 빈 파일을 삭제하므로, 그 파일에 나중에 성공적으로 쓰지 않는 한 openFile을 WriteMode로 수행한 후에도 그 파일이 존재한다는 보장은 없어요. mode가 AppendMode이면 핸들은 파일의 끝에 위치하고, 그 외에는 시작 부분에 위치해요(이 경우 내부 위치는 0). 초기 버퍼 모드는 구현에 따라 달라져요. 이 연산은 다음 경우에 실패할 수 있어요: 파일이 이미 열려 있어 다시 열 수 없으면 isAlreadyInUseError, 파일이 존재하지 않으면 isDoesNotExistError, 사용자에게 파일을 열 권한이 없으면 isPermissionError.
data IOMode
= ReadMode | WriteMode | AppendMode | ReadWriteMode
System.IO.openFile을 참고해요.
instance Enum IOMode instance Eq IOMode instance Ord IOMode instance Read IOMode instance Show IOMode instance Ix IOMode
41.3.2 파일 닫기
hClose :: Handle -> IO ()
계산 hClose hdl은 핸들 hdl을 닫혀 있는 상태로 만들어요. 계산이 끝나기 전에, hdl이 쓰기 가능하면 그 버퍼는 hFlush에서처럼 플러시돼요. 이미 닫힌 핸들에 hClose를 수행하는 것은 아무 효과가 없고, 그렇게 하는 것은 오류가 아니에요. 닫힌 핸들에 대한 다른 모든 연산은 실패해요. 어떤 이유로든 hClose가 실패하면, 핸들에 대한 (hClose 외의) 이후 모든 연산은 hdl이 성공적으로 닫힌 것처럼 여전히 실패해요.
41.3.3 특수한 경우들
이 함수들은 Prelude에서도 내보내집니다.
readFile :: FilePath -> IO String
readFile 함수는 파일을 읽고 그 내용을 문자열로 돌려줘요. getContents에서처럼 파일은 게으르게, 요청에 따라 읽혀요.
writeFile :: FilePath -> String -> IO ()
계산 writeFile file str 함수는 문자열 str을 파일 file에 써요.
appendFile :: FilePath -> String -> IO ()
계산 appendFile file str 함수는 문자열 str을 파일 file에 덧붙여요. writeFile과 appendFile은 리터럴 문자열을 파일에 쓴다는 점을 기억해요. print처럼 인쇄 가능한 타입의 값을 쓰려면, 먼저 show 함수로 그 값을 문자열로 변환해야 해요.
main = appendFile "squares" (show [(x,x⋆x) | x <- [0,0.1..2]])
41.3.4 파일 잠금
구현은 가능한 한, 적어도 Haskell 프로세스 로컬 범위에서는, 파일에 대해 다중-판독자-단일-기록자(multiple-reader single-writer) 잠금을 강제해야 해요. 즉, 같은 파일에 입력을 관리하는 핸들이 여러 개 있을 수 있거나, 출력을 관리하는 핸들이 단 하나만 있을 수 있어요. 열려 있거나 반쯤 닫힌 핸들이 어떤 파일을 출력용으로 관리하고 있다면, 그 파일에 대해 새 핸들을 할당할 수 없어요. 열려 있거나 반쯤 닫힌 핸들이 파일을 입력용으로 관리하고 있다면, 출력을 관리하지 않는 경우에만 새 핸들을 할당할 수 있어요. 두 파일이 같은 파일인지 여부는 구현에 따라 달라지지만, 보통 같은 절대 경로 이름을 갖고 둘 중 하나도 (예를 들어) 이름이 바뀌지 않았다면 같은 파일이어야 해요. 경고: readFile 연산은 파일의 전체 내용이 소비될 때까지 그 파일에 대한 반쯤 닫힌 핸들을 유지해요. 따라서 앞서 readFile로 열린 파일에 (예를 들어 writeFile로) 쓰기를 시도하면, 대개 System.IO.Error.isAlreadyInUseError로 실패하게 돼요.
41.4 핸들에 대한 연산
41.4.1 파일 크기 결정과 변경
hFileSize :: Handle -> IO Integer
물리 파일에 붙어 있는 핸들 hdl에 대해, hFileSize hdl은 그 파일의 크기를 8비트 바이트 단위로 돌려줘요.
hSetFileSize :: Handle -> Integer -> IO ()
hSetFileSize hdl size는 핸들 hdl을 가진 물리 파일을 size 바이트로 잘라내요.
41.4.2 입력의 끝 감지
hIsEOF :: Handle -> IO Bool
읽기 가능한 핸들 hdl에 대해, hIsEOF hdl은 hdl에서 더 이상 입력을 받을 수 없거나, 물리 파일의 경우 현재 I/O 위치가 파일 길이와 같으면 True를 돌려줘요. 그 외에는 False를 돌려줘요. 주의: hIsEOF는 데이터가 더 있는지 알아내기 위해 스트림에서 읽기를 시도해야 하므로 블록될 수 있어요.
isEOF :: IO Bool
계산 isEOF는 stdin에서만 동작한다는 점을 제외하면 hIsEOF와 동일해요.
41.4.3 버퍼링 연산
data BufferMode
= NoBuffering -- 가능하면 버퍼링을 비활성화
| LineBuffering -- 가능하면 라인 버퍼링을 활성화
| BlockBuffering (Maybe Int) -- 가능하면 블록 버퍼링을 활성화.
-- 인자가 Just n이면 버퍼 크기는 n 항목이고, 그 외에는 구현에 따라 달라짐
세 종류의 버퍼링이 지원돼요: 라인 버퍼링, 블록 버퍼링, 또는 버퍼링 없음. 이 모드들은 다음과 같은 효과를 가져요. 출력의 경우 항목들은 버퍼 모드에 따라 내부 버퍼에서 기록되거나 플러시돼요: 라인 버퍼링 — 새 줄이 출력되거나, 버퍼가 넘치거나, System.IO.hFlush가 발행되거나, 핸들이 닫힐 때마다 전체 출력 버퍼가 플러시돼요. 블록 버퍼링 — 버퍼가 넘치거나, System.IO.hFlush가 발행되거나, 핸들이 닫힐 때마다 전체 버퍼가 기록돼요. 버퍼링 없음 — 출력은 즉시 기록되고 버퍼에 저장되지 않아요. 구현은 버퍼를 위에서 지정한 것보다 더 자주 플러시해도 되지만, 덜 자주는 안 돼요. 출력 버퍼는 기록되자마자 비워져요. 비슷하게, 입력도 핸들의 버퍼 모드에 따라 일어나요: 라인 버퍼링 — 핸들의 버퍼가 비어 있지 않으면 다음 항목을 버퍼에서 얻고, 버퍼가 비어 있으면 다음 새 줄 문자까지 포함해 문자들을 버퍼로 읽어요. 새 줄 문자가 준비되거나 버퍼가 가득 차기 전까지는 문자를 사용할 수 없어요. 블록 버퍼링 — 핸들의 버퍼가 비면 다음 데이터 블록을 버퍼로 읽어요. 버퍼링 없음 — 다음 입력 항목을 읽어 반환해요. System.IO.hLookAhead 연산은 버퍼링 없는 핸들조차 한 문자 버퍼가 필요할 수 있다는 뜻이에요. 핸들이 열릴 때의 기본 버퍼링 모드는 구현에 따라 달라지며, 그 핸들에 붙어 있는 파일 시스템 객체에 따라 달라질 수 있어요. 대부분의 구현에서 물리 파일은 보통 블록 버퍼링되고, 터미널은 보통 라인 버퍼링돼요.
instance Eq BufferMode instance Ord BufferMode instance Read BufferMode instance Show BufferMode
hSetBuffering :: Handle -> BufferMode -> IO ()
계산 hSetBuffering hdl mode는 핸들 hdl의 이후 읽기·쓰기에 대한 버퍼링 모드를 설정해요. 버퍼 모드가 BlockBuffering이나 LineBuffering에서 NoBuffering으로 바뀌면, hdl이 쓰기 가능하면 버퍼는 hFlush에서처럼 플러시되고, 쓰기 가능하지 않으면 버퍼의 내용은 버려져요. 이 연산은, 핸들이 이미 읽기·쓰기에 사용됐고 구현이 버퍼링 모드 변경을 허용하지 않으면 isPermissionError로 실패할 수 있어요.
hGetBuffering :: Handle -> IO BufferMode
계산 hGetBuffering hdl은 hdl의 현재 버퍼링 모드를 돌려줘요.
hFlush :: Handle -> IO ()
동작 hFlush hdl은 핸들 hdl에서 출력용으로 버퍼링된 모든 항목을 즉시 운영체제로 보내요. 이 연산은, 장치가 가득 차면 isFullError, 시스템 리소스 한도를 초과하면 isPermissionError로 실패할 수 있어요. 이러한 상황에서 버퍼의 문자들이 버려지는지 유지되는지는 명시되지 않아요.
41.4.4 핸들 위치 재배치
hGetPosn :: Handle -> IO HandlePosn
계산 hGetPosn hdl은 hdl의 현재 I/O 위치를 추상 타입 HandlePosn의 값으로 돌려줘요.
hSetPosn :: HandlePosn -> IO ()
hGetPosn hdl 호출이 위치 p를 돌려줬다면, 계산 hSetPosn p는 hdl의 위치를 hGetPosn 호출 시점에 가졌던 위치로 설정해요. 이 연산은, 시스템 리소스 한도를 초과하면 isPermissionError로 실패할 수 있어요.
data HandlePosn
instance Eq HandlePosn instance Show HandlePosn
hSeek :: Handle -> SeekMode -> Integer -> IO ()
계산 hSeek hdl mode i는 mode에 따라 핸들 hdl의 위치를 설정해요. 오프셋 i는 8비트 바이트 단위로 주어져요. hdl이 블록·라인 버퍼링이라면, 현재 버퍼에 없는 위치로 탐색하는 것은 먼저 출력 버퍼의 항목들을 장치에 기록하게 하고, 그 다음 입력 버퍼를 버리게 해요. 어떤 핸들은 탐색이 불가능할 수도 있고(hIsSeekable 참고), 가능한 위치 연산의 일부만 지원할 수도 있어요(예를 들어 테이프의 끝으로나, 시작 또는 현재 위치로부터 양의 오프셋으로만 탐색할 수 있는 경우). 음의 I/O 위치를 설정하는 것은 불가능하고, 물리 파일의 경우 현재 파일 끝 너머의 I/O 위치를 설정하는 것도 불가능해요. 이 연산은, 핸들이 탐색 가능하지 않거나 요청된 탐색 모드를 지원하지 않으면 isIllegalOperationError, 시스템 리소스 한도를 초과하면 isPermissionError로 실패할 수 있어요.
data SeekMode
= AbsoluteSeek -- hdl의 위치를 i로 설정
| RelativeSeek -- hdl의 위치를 현재 위치로부터 오프셋 i로 설정
| SeekFromEnd -- hdl의 위치를 파일 끝으로부터 오프셋 i로 설정
hSeek hdl mode i의 효과를 결정하는 모드예요.
instance Enum SeekMode instance Eq SeekMode instance Ord SeekMode instance Read SeekMode instance Show SeekMode instance Ix SeekMode
hTell :: Handle -> IO Integer
계산 hTell hdl은 핸들 hdl의 현재 위치를 파일 시작으로부터의 바이트 수로 돌려줘요. 반환된 값은 나중에 hSeek에 전달해 핸들을 현재 위치로 재배치할 수 있어요. 이 연산은, 핸들이 탐색 가능하지 않으면 isIllegalOperationError로 실패할 수 있어요.
41.4.5 핸들 속성
이 연산들 각각은 핸들이 지정된 속성을 가지면 True를, 그렇지 않으면 False를 돌려줘요.
hIsOpen :: Handle -> IO Bool
hIsClosed :: Handle -> IO Bool
hIsReadable :: Handle -> IO Bool
hIsWritable :: Handle -> IO Bool
hIsSeekable :: Handle -> IO Bool
41.4.6 터미널 연산
hIsTerminalDevice :: Handle -> IO Bool
핸들이 터미널에 연결되어 있나요?
hSetEcho :: Handle -> Bool -> IO ()
터미널에 연결된 핸들의 에코 상태를 설정해요.
hGetEcho :: Handle -> IO Bool
터미널에 연결된 핸들의 에코 상태를 가져와요.
41.4.7 핸들 상태 표시
hShow :: Handle -> IO String
hShow는 IO 모나드 안에 있으며, Handle에 대한 (순수한) Show 인스턴스보다 더 포괄적인 출력을 제공해요.
41.5 텍스트 입력과 출력
41.5.1 텍스트 입력
hWaitForInput :: Handle -> Int -> IO Bool
계산 hWaitForInput hdl t는 핸들 hdl에 입력이 준비될 때까지 기다려요. hdl에 입력이 준비되는 즉시 True를, t 밀리초 안에 입력이 없으면 False를 돌려줘요. hWaitForInput은 하나 이상의 완전한 문자가 준비될 때까지 기다리므로, 디코딩을 수행해야 하고 따라서 디코딩 오류로 실패할 수 있다는 점을 기억해요. t가 0보다 작으면 hWaitForInput은 무기한 기다려요. 이 연산은, 파일 끝에 도달했으면 isEOFError, 입력이 이 핸들의 인코딩에서 유효하지 않은 바이트 시퀀스로 시작하면 디코딩 오류로 실패할 수 있어요.
hReady :: Handle -> IO Bool
계산 hReady hdl은 핸들 hdl로부터 입력용 항목이 적어도 하나 준비되어 있는지 나타내요. 이 연산은, 파일 끝에 도달했으면 System.IO.Error.isEOFError로 실패할 수 있어요.
hGetChar :: Handle -> IO Char
계산 hGetChar hdl은 hdl이 관리하는 파일·채널에서 문자를 읽는데, 문자가 준비될 때까지 블록돼요. 이 연산은, 파일 끝에 도달했으면 isEOFError로 실패할 수 있어요.
hGetLine :: Handle -> IO String
계산 hGetLine hdl은 hdl이 관리하는 파일·채널에서 한 줄을 읽어요. 이 연산은, 줄의 첫 문자를 읽을 때 파일 끝을 만나면 isEOFError로 실패할 수 있어요. hGetLine이 줄을 읽는 동안 다른 지점에서 파일 끝을 만나면, 그것은 줄 종결자로 취급되고 (부분적인) 줄이 반환돼요.
hLookAhead :: Handle -> IO Char
계산 hLookAhead는 핸들에서 다음 문자를, 입력 버퍼에서 제거하지 않고 돌려주는데, 문자가 준비될 때까지 블록돼요. 이 연산은, 파일 끝에 도달했으면 isEOFError로 실패할 수 있어요.
hGetContents :: Handle -> IO String
계산 hGetContents hdl은 hdl이 관리하는 채널·파일의 안 읽은 부분에 대응하는 문자 목록을 돌려주는데, hdl은 중간 상태인 반쯤 닫힌(semi-closed) 상태로 들어가요. 이 상태에서 hdl은 사실상 닫히지만, 항목들은 요청에 따라 hdl에서 읽혀 hGetContents hdl이 돌려주는 특별한 목록에 축적돼요. 핸들이 닫혔기 때문에 실패하는 연산은, 핸들이 반쯤 닫혀 있어도 실패해요. 유일한 예외는 hClose예요. 반쯤 닫힌 핸들은 다음 경우에 닫혀지게 돼요: hClose가 적용되거나; 핸들에서 항목을 읽을 때 I/O 오류가 발생하거나; 핸들의 전체 내용이 읽히면. 반쯤 닫힌 핸들이 닫히면, 연관된 목록의 내용이 고정돼요. 이 최종 목록의 내용은 부분적으로만 지정돼요: 핸들이 닫히기 전에 평가된 스트림의 항목은 모두 포함할 거예요. 핸들이 반쯤 닫혀 있는 동안 만난 I/O 오류는 그냥 버려져요. 이 연산은, 파일 끝에 도달했으면 isEOFError로 실패할 수 있어요.
41.5.2 텍스트 출력
hPutChar :: Handle -> Char -> IO ()
계산 hPutChar hdl ch는 문자 ch를 hdl이 관리하는 파일·채널에 써요. hdl에 버퍼링이 활성화되어 있으면 문자는 버퍼링될 수 있어요. 이 연산은, 장치가 가득 차면 isFullError, 다른 시스템 리소스 한도를 초과하면 isPermissionError로 실패할 수 있어요.
hPutStr :: Handle -> String -> IO ()
계산 hPutStr hdl s는 문자열 s를 hdl이 관리하는 파일·채널에 써요. 이 연산은, 장치가 가득 차면 isFullError, 다른 시스템 리소스 한도를 초과하면 isPermissionError로 실패할 수 있어요.
hPutStrLn :: Handle -> String -> IO ()
hPutStr와 같지만, 새 줄 문자를 추가해요.
hPrint :: Show a => Handle -> a -> IO ()
계산 hPrint hdl t는 shows 함수가 주는 t의 문자열 표현을 hdl이 관리하는 파일·채널에 쓰고 새 줄을 덧붙여요. 이 연산은, 장치가 가득 차면 System.IO.Error.isFullError, 다른 시스템 리소스 한도를 초과하면 System.IO.Error.isPermissionError로 실패할 수 있어요.
41.5.3 표준 입력·출력의 특수한 경우
이 함수들은 Prelude에서도 내보내집니다.
interact :: (String -> String) -> IO ()
interact 함수는 String->String 타입의 함수를 인자로 받아요. 표준 입력 장치의 전체 입력이 이 함수에 인자로 전달되고, 결과 문자열이 표준 출력 장치로 출력돼요.
putChar :: Char -> IO ()
표준 출력 장치에 문자를 써요(hPutChar stdout과 같음).
putStr :: String -> IO ()
표준 출력 장치에 문자열을 써요(hPutStr stdout과 같음).
putStrLn :: String -> IO ()
putStr과 같지만, 새 줄 문자를 추가해요.
print :: Show a => a -> IO ()
print 함수는 인쇄 가능한 타입의 값을 표준 출력 장치로 출력해요. 인쇄 가능한 타입은 Show 클래스의 인스턴스인 타입들이에요. print는 show 연산을 사용해 값을 출력용 문자열로 변환하고 새 줄을 추가해요. 예를 들어 처음 20개의 정수와 그 2의 거듭제곱을 출력하는 프로그램은 다음과 같이 쓸 수 있어요.
main = print ([(n, 2^n) | n <- [0..19]])
getChar :: IO Char
표준 입력 장치에서 문자를 읽어요(hGetChar stdin과 같음).
getLine :: IO String
표준 입력 장치에서 한 줄을 읽어요(hGetLine stdin과 같음).
getContents :: IO String
getContents 연산은 모든 사용자 입력을 하나의 문자열로 돌려주는데, 필요할 때 게으르게 읽혀요(hGetContents stdin과 같음).
readIO :: Read a => String -> IO a
readIO 함수는 read와 비슷하지만, 프로그램을 종료시키는 대신 파싱 실패를 IO 모나드에 신호로 알려줘요.
readLn :: Read a => IO a
표준 입력에서 값을 읽고(getLine 후 readIO), 파싱 실패를 IO 모나드에 신호로 알려줘요.