기본 입출력
기본 입출력 (Basic Input/Output)
Haskell은 순수 함수형 언어라서 값이 외부 세계와 직접 상호작용하지 않아요. 그런데 실제 프로그램은 파일도 읽고 화면에도 출력해야 하죠. Haskell은 그 "외부 세계와의 통신"을 IO 모나드라는 구조로 안전하게 감싸 처리해요. 이 장은 입출력 연산의 종류와 그 의미를 다뤄요.
본문
이 장은 Haskell의 입출력(I/O) 시설, 즉 Haskell 프로그램이 외부 세계와 어떻게 소통하는지를 설명해요. Haskell에서 I/O는 순수 함수와 분리되어, IO 모나드라는 타입 생성자로 캡슐화돼요.
IO a 타입의 값은 **액션(action)**이에요. 실행되면 외부 세계와 상호작용하고 타입 a의 값을 돌려주는 계산이죠. 프로그램의 엔트리 포인트 main은 어떤 타입 τ에 대해 타입 IO τ의 계산이어야 해요(5절, 6.1.7절). 프로그램이 실행되면 계산 main이 수행되고, 그 결과(타입 τ)는 버려져요.
IO 타입은 추상적이에요. 사용자에게 보이는 생성자가 없기 때문에, 액션은 IO 모나드의 연산과 do 표기법(3.14절)으로만 조합할 수 있어요.
기본 I/O 연산
Prelude(9장)는 기본 I/O 액션을 제공해요. 주요 출력 연산:
putChar :: Char -> IO ()
putStr :: String -> IO ()
putStrLn :: String -> IO ()
print :: Show a => a -> IO ()
putChar c는 문자c를 표준 출력에 써요.putStr s는 문자열s를 써요.putStrLn s는 마지막에 개행(newline)을 추가해요.print x는show x의 결과를 출력하고 개행을 추가해요(putStrLn . show).
주요 입력 연산:
getChar :: IO Char
getLine :: IO String
getContents :: IO String
interact :: (String -> String) -> IO ()
getChar는 표준 입력에서 문자 하나를 읽어요.getLine은 입력에서 줄 하나를 읽고, 끝의 개행을 뺀 문자열을 돌려줘요.getContents는 표준 입력 전체를 하나의 지연된(lazy) 문자열로 돌려줘요.interact f는 표준 입력 전체를 함수f에 넘기고 그 결과를 표준 출력에 써요.cat같은 필터 프로그램을 쓰기에 좋아요.
액션들은 do 식으로 순차 조합할 수 있어요. 예:
main = do
putStr "이름을 입력하세요: "
name <- getLine
putStrLn ("안녕, " ++ name ++ "!")
<-는 액션의 결과를 변수에 바인딩하고, 이후 액션들은 그 값을 쓸 수 있어요. do 표기법은 3.14절에 있는 것처럼 >>=와 >> 연산의 문법적 설탕이에요. 즉 name <- getLine은 getLine >>= \name -> ...에 해당해요. 모나드 연산에 대해서는 6.3.6절을 봐요.
지연된(lazy) I/O
getContents 같은 액션은 입력을 느긋하게 읽어, 프로그램이 실제로 요구하는 만큼만 입력을 소비할 수 있게 해 줘요. 이 덕분에 무한 입력을 값으로 다루는 함수형 스타일도 가능해져요. 예를 들어 interact는 cat 같은 전형적인 필터 프로그램을 한 줄로 쓰게 해 줘요:
main = interact (unlines . map reverse . lines)
이 프로그램은 각 입력 줄을 뒤집어 출력해요.
파일과 핸들(Handles)
파일은 저수준 인터페이스를 제공해요. 파일을 열면 Handle을 얻고, 이 핸들을 통해 읽기·쓰기를 수행해요. 표준 라이브러리 모듈 System.IO가 다뤄요:
openFile :: FilePath -> IOMode -> IO Handle
hGetLine :: Handle -> IO String
hPutStr :: Handle -> String -> IO ()
hClose :: Handle -> IO ()
IOMode는 ReadMode, WriteMode, ReadWriteMode, AppendMode 중 하나예요. 파일 경로는 type FilePath = String이에요.
편의를 위한 고차원 함수들도 있어요. 파일을 열고 내용을 통째로 처리한 뒤 닫는 일을 한 번에 해 줘요:
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
appendFile :: FilePath -> String -> IO ()
핸들 연산은 표준 입력·출력·오류 스트림에도 적용할 수 있는데, 미리 정의된 핸들 stdin, stdout, stderr로 접근해요.
I/O 오류 (IOErrors)
I/O 연산이 실패하면 IOError 타입의 예외를 일으켜요(6.1.7절). 예를 들어 존재하지 않는 파일을 읽으려 하면 오류가 나요. 프로그램은 그 오류를 잡아 처리할 수 있는데, Haskell 2010에서는 System.IO.Error 모듈의 함수로 다뤄요. IOError는 Show와 Eq의 인스턴스이지만, 그 값은 다양한 I/O 함수가 만들고 이 리포트에서 더 자세히 정의되지는 않아요.
더 알아보기 (Learn more)
- 6장 미리 정의된 타입과 클래스 (Predefined Types and Classes) —
IO타입,Monad클래스,IOError타입을 봐요. - 3장 식 (Expressions) — 3.14절의
do식과 그 번역을 봐요. - 9장 표준 Prelude (Standard Prelude) — Prelude가 정의한 I/O 함수를 실제로 봐요.