Functional Programming - Interactive Programs
초창기 Haskell은 다른 언어들과 달리 그 효용성을 증명하는 것이 어려웠습니다. 그 이유는 다른 언어들을 이용할 때는 쉽게 할 수 있는 외부와의 소통(communication)의 어려움이었습니다. 예를 들어, 우리는 어떤 언어를 사용하든 'hello world!'를 출력하였습니다.
System.out.println("hello world!");
위 코드는 standard output을 이용하여 사용자에게 결과를 보여주었습니다. 하지만, haskell은 사용자로부터 input/ouput을 주고 받기위한 I/O Stream이 존재하지 않았습니다.
오늘날 haskell은 I/O monad를 통해 외부와 소통할 수 있게 되었습니다.
이때까지 input을 받고 string, integer 또는 tuple과 같은 output을 돌려주는게 전부였습니다. 하지만, C# 또는 Java와 같이 계속해서 새로운 input을 받아 결과를 돌려줄 수 있는 프로그램을 원했습니다.
이를 위해 실행 중 키보드(keyboard)로 부터 결과를 입력받아 화면(screen)에 출력하며 서로 연결된(interactive) 프로그램을 만들어야햇습니다.
The Problem
하지만, 문제가 있습니다. Haskell 프로그램은 순수한 수학적 함수(pure mathematical functions)입니다.
Haskell programs have no side effects.
순수한 수학적 함수란 항상 동일한 입력에 대해 동일한 결과를 돌려주는 것을 의미합니다. 예를 들어 factorial n
은 어떠한 경우에서도 같은 결과를 돌려줍니다.
하지만, readLine
이라는 Java 함수의 경우 아무런 입력(argument)을 받지 않으며 다양한 결과(String)을 돌려줍니다. 이런 이유로 readLine
은 수학적 함수가 아니며 side effects을 가질 수 있습니다. String을 돌려주지만, 그 type이 명확하지 않기 때문입니다. 이것이 I/O monad가 해결해주는 것입니다. I/O monad는 side effect를 가지는 함수의 type안에서 표현(express)합니다.
정리하자면 외부와 소통하기 위해(키보드로 입력받고 화면으로 출력하는) side effect를 가질 수 밖에 없습니다.
Interactive programs have side effects.
따라서 우리는 haskell 프로그램 안에서 side effect를 포함시키는 방안이 필요합니다.
side effect는 꼭 나쁘다는게 아닙니다. side effect가 없다면 interactive program
을 만드는 것이 어렵기 때문입니다. 따라서, 중요한 것은 side effect를 어떻게 밸런스있게 다루냐는 겁입니다.
The Solution
Haskell으로 작성된 interactive program
은 순수한 표현(pure expressions)와 구분하기 위해 side effect를 가질 수 있는 순수하지 않은(impure) action을 표현하는 방법이 있습니다.
IO a
이 IO
라는 표현은 Haksell 안에서 actions
을 의미하는 동시에 지금까지 언급하지 않았던 Haskell의 statement를 의미하기도 합니다. (하지만, actions
또한 expression이므로, 구성이 가능합니다.)
다음으로 구체적인 예를 보겠습니다.
IO Char
IO ()
위는 side effect를 가지지만 한 글자를 돌려주는 action type입니다. 아래는 가장 명력적인(imperative) type인 IO unit입니다. 이것은 오직 side effect만을 수행하는데, 이유는 어떠한 실제적인 결과값을 돌려주는 것이 아닌 비어있는 tuple을 돌려주기 때문입니다. 다른 기타 언어에서는 이를 void로 표현합니다.
앞으로 우리는 앞에서 보았던 I/O Monad를 이용하지만, 어떻게 구성되어있는지 신경쓰지 않습니다. 이건 마치 Java의 OOP(Object Oriented Programming) class개념과 비슷합니다. 우리는 그러한 class를 이용할 뿐입니다.
Basic Actions
Standard Library는 많은 actions을 제공하며 다음 3가지를 포함합니다.
getChar
action은 키보드로부터 한 글자를 읽어 screen에 보여주며 값으로 돌려줍니다. 만일 haskell이 엄격하다면(strict) 결과를 바로 돌려주지만, 게이른(lazy) 언어이기 때문에 바로 평가하지 않습니다.
getChar :: IO Char
putChar c
action은 화면에 한글자를 출력하며 결과를 돌려주지 않습니다.
putChar :: Char -> IO ()
return v
action은 단순히 결과를 돌려줍니다. 마치 앞에서 배운 Parser와 비슷한 동작을 보여줍니다.
return :: a -> IO a
Sequencing
여러 action을 하나의 sequence로 표현할 수 있습니다. Parser의 경우와 동일하게 do
를 이용합니다.
a :: IO (Char, Char)
a = do x <- getChar
getChar
y <- getChar
return (x,y)
Derived Primitives
getchar
를 이용하여 한줄을 읽어 stirng을 돌려주는 함수를 만들 수 있습니다.
getLine :: IO String
getLine = do x <- getChar
if x == '\n' then
return []
else
do xs <- getLine
return (x:xs)
여기서 눈여겨봐야 하는 점은 getChar
는 IO Char
type을이지만, x
의 경우 Char
type 입니다. 따라서 =
이 아닌 list comprehension 경우와 동일하게 <-
를 이용하여 표현합니다.
이와 비슷하게 아래 함수들도 정의 할 수 있습니다.
- String을 화면에 출력하는 함수입니다.
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do putChar x
return xs
- 한줄을 출력하고 다음줄로 넘어가는 함수입니다.
putStrLn :: String -> IO ()
putStrLn xs = do putChar xs
putChar '\n'
Example
위를 종합하여 가장 놀라운 것은 이 모든 것들을 우리가 이전에 배웠던 모든 함수들과 함께 조합할 수 있다는 겁니다. 특히 list를 입력으로 받는 함수 경우 string 또한 list이기 때문에 적용이 가능합니다.
strLen :: IO ()
strLen = do putStr "Enter a string: "
xs <- getLine
putStr "The string has "
putStr (show (length xs))
putStrLn " characters"
이 예에서도 IO Monad와 함께 length
를 섞어 사용하고 있습니다.
> strlen
Enter a string: abcde
The string has 5 characters
중요한 것은 action에 대한 평가는 side effect를 실행하지만 결과를 버리게 됩니다. 이유는 IO unit 이기 때문입니다.
Hangman
앞서 우리는 명령적(imperative) 부분에 순수한(pure) 코드를 도입하는 것을 보았습니다. 유명한 게임 중 하나인 Hangman을 보면서 설명을 이어가겠습니다.
한 플레이어가 몰래 단어를 작성합니다.
다른 플레이어가 여러 가정들을 입력하면서 그 단어를 유추합니다.
컴퓨터는 처음 작성된 단어안 글자 중에 가정들속에서 단어에 포함된 글자를 나타냅니다.
게임은 유추가 맞을 경우 끝납니다.
이러한 Hangman을 top down 방식으로 다음과 같이 만들 수 있습니다.
hangman :: IO ()
hangman =
do putStrLn "Think of a word: "
word <- sgetLine
putStrLn "Try to guess it: "
guess word
일단 sgetLine
을 통해 값을 입력받습니다. 여기서 중요한 것은 sgetLine
은 string을 읽어오지만, 화면에는 (_)로 나타내야합니다. 이렇게 받은 단어를 guess
를 통해 유추하게 됩니다.
sgetLine :: IO String
sgetLine = do x <- getCh
if x == '\n' then
do putChar x
return []
else
do putChar '-'
xs <- sgetLine
return (x:xs)
위는 sgetLine
을 구현한 것입니다. 이전에 봤던 getLine
과 비교할 때 putchar '-'
와 getCh
부분이 변경된 것 이외에 동일합니다. 여기서 getCh
는 값을 입력받지만 화면에 출력하지 않는 함수로 getChar
와 다르게 동작해야합니다. 아래와 같이 구현이 가능합니다.
import System.IO
getCh :: IO Char
getCh = do hSetEcho stdin False
c <- getChar
hSetEcho stdin True
return c
hSetEcho stdin
을 False
또는 True
를 통해 echo 셋팅을 껏다 키는 것을 확인 할 수 있습니다. 여기서도 중요한 것은 getChar
를 통해 c
에 값을 대입하는 것이 아니라 bind한다는 것입니다. 여기서 c
는 고정된(not mutable) 값으로 side effect를 가질 수 있습니다. Haskell에서도 변경가능한(mutable) 변수가 있지만 여기서는 다루지 않겠습니다.
guess :: String -> IO ()
guess word =
do putStr "> "
xs <- getLine
if xs == word then
putStrLn "You got it!"
else
do putStrLn (diff word xs)
guess word
위는 guess
를 구현한 예제입니다. 단어가 맞으면 프로그램은 종료되지만, 맞추기 못할 경우 앞서 받은 word와 다른 글자를 돌려주며 다시 guess
를 호출합니다.
diff :: String -> String -> String
diff xs ys =
[if elem x ys then x else '-' | x <- xs]
> diff "haskell" "pascal"
"-as--ll"
위 diff
함수는 이전에 다뤘던 순수한(pure) 함수형 언어로 구현되어있습니다.