본문 바로가기

Haskell

몇 가지 간단한 연습문제

기타 몇 가지 하스켈 초급 문제들을 해결해보고, 입출력을 처리하는 과정을 살펴보자.

리스트를 뒤집기

일련의 리스트를 입력된 순서를 거꾸로하여 출력한다. 단, reverse 함수를 쓰지 말 것. (https://www.hackerrank.com/challenges/fp-reverse-a-list/problem)

이 문제는 reverse 함수를 구현해보라는 의미이다. 예를 들어 1 2 3 의 리스트가 있다고 할 때, 이를 (2 3을 뒤집은 결과) + [1] 로 만드는 처리를 재귀적으로 수행하면 역순의 리스트를 만들 수 있다. 따라서 다음과 같은 함수를 정의할 수 있다.

rev :: [a] -> [a]
rev [] = []
rev (x:xs) = (rev xs) ++ [x]

실제로 이 구현은 '매우 정직한' 뒤집기 함수로서 잘 동작한다. 그 외에도 여러 가지 방법이 있을 수 있다.

import Data.List ((!!))
rev2 lst = let l = length lst in [lst !! (l-x) | x <- [1..l]

하지만 그 중에 가장 좋은 것은 foldl을 이용하는 방법이다.

foldl :: (Foldable t) => (b -> a -> b) -> b -> t a -> b

foldl의 타입에서 알 수 있듯이 누적값과 새 원소를 이용해서 새 누적값을 만드는 함수와 초기값, 그리고 접기가 가능한 a의 구조를 통해서 reduce 하는 함수인데, 앞에서부터 접어서 나간다.

만약 첫 누적값이 []이고 리스트의 앞에서부터 이 누적값에 x:[] 와 같은 식으로 덧붙여나가면 4:3:2:1:[] 이런 식으로 뒤집은 리스트를 만들 수 있을 것이다.

따라서,

rev = foldl (\xs y -> y:xs) []

이렇게 정의할 수 있다.

최종코드

-- h005.hs
import Control.Monad

rev = foldl (\xs y -> y:xs) []
main = getContents >>= mapM_ print . rev . words

홀수값의 합

주어진 리스트를 정수의 리스트로 읽어내고, 그 중 홀수만 추려서 합을 구한다. 이는 odd 함수로 필터링해서 sum하면 끝

-- h006.hs
f = sum . filter odd
main = getContents >>= print . f . (map read) . words

리스트 갱신

리스트는 '갱신가능한' 타입이 아니다. 대신에 함수에 넣어서 다른 리스트로 바꿔야 한다. 이 문제에서는 입력받은 일련의 정수리스트의 절대값을 출력한다.

f = map abs

main = do
  inputData <- getContents
  mapM_ putStrLn . map show . f . map (read :: String -> Int) . lines $ inputData

최대공약수 구하기

유클리드의 호제법을 이용한 최대공약수를 구하는 문제이다. 다른 언어에서도 재귀를 이용해서 구하므로 어렵지 않다.

gcd' :: Integral a => a -> a -> a -> a
gcd' n m = 
  | m == 0 = n
  | otherwise = gcd' m (n `mod` m)
  
main :: IO ()
main = getContents >>= 
	putStrLn . show . (uncurry gcd') . list2tuple . map read . words
  where list2tuple (x:y:_) = (x, y)

참고로 하스켈의 기본 라이브러리인 Prelude에는 gcd, lcm 함수를 이미 구비하고 있다. 따라서 '을 붙여서 별개의 함수로 이름 지었다.

입출력 흐름을 잘 살펴보자.

  1. 입력된 라인을 공백단위로 쪼개고 (words)
  2. 쪼개진 단어들을 정수로 변환하도록 맵핑한다. (map read)
  3. 이제 정수 2개짜리 리스트를 튜플로 변환하고 (list2tuple)
  4. 이 값을 튜플을 받도록 변경된 (uncurry gcd') gcd' 함수에 전달한다.
  5. 그 결과를 문자열로 변환해서 show
  6. 화면에 출력한다. putStrLn

이 흐름은 기본적으로 문자열을 받아서 IO 액션으로 끝났다. 따라서 String -> IO ()의 모양이 된다.

>>= 은 모나드에 대해서 Monad m => m a -> (a -> m b) -> m b타입의 연산으로 String -> IO ()IO String 값을 주입하는 역할을 한다.

이제 해당 처리 과정이 눈에 들어오는지?