본문 바로가기

Haskell

연습 - 문자열 섞기

문자열의 각 글자를 교대로 뒤섞는다.

출처 : https://www.hackerrank.com/challenges/string-mingling/problem

두 줄의 문자열을 입력받아서 글자가 교대로 나타나도록 두 문자열을 하나로 합성한다.

예: hello world -> hweolrllod

문자열을 합성하는 것은 간단하다. 문자열은 [Char] 타입의 별칭이므로 zipWith를 이용해서 간단히 합성할 수 있다.

mingle :: String -> String -> String
mingle s1 s2 = concat . zipWith (\x y -> [x,y]) s1 $ s2

그리고 main 함수는 다음과 같이 쓰면 된다.

main :: IO ()
main = do
	s1 <- getLine
	s2 <- getLine
	putStrLn $ mingle s1 s2

그런데 이전의 문제에서처럼 do 블럭을 사용하지 않고 getContents >>= ...를 쓰는 것 처럼 한줄로 이 함수를 표현할 방법이 없을까? 방법을 찾기 위해서 일단 관련되는 각 함수의 타입을 보자. 타입을 살펴보는 것만으로 충분히 많은 힌트를 얻을 수 있다.

먼저 getLine의 타입은 IO String 이고, mingleString -> String -> String이다. 따라서 mingle을 getLine에 맵핑하면 IO (String -> String) 타입의 적용성 펑터가 하나 나오게 된다. (IO 액션은 모나드인 동시에 적용성 펑터이기도 하다)

mingle <$> getLine :: IO (String -> String)

적용성 펑터는 <*>를 통해서 다른 적용성 펑터에 적용될 수 있다. (f (a -> b) -> f a -> f b) 따라서 mingle <$> getLine <*> getLine 하면 IO String이 된다.

다시 >>=m a -> (a -> m b) -> m b가 되므로 보통 IO String을 String -> IO ()로 만드는 일에 쓸 수 있다. 즉, putStrLn에 붙일 수 있다.

따라서 다음과 같이 쓰면

main = mingle <$> getLine <*> getLine >>= putStrLn

뭔가 완전 말도 안되는 것 같지만 우변은 완벽하게 IO ()가 되고 실제로도 잘 동작한다!!!!!

응용 문제

https://www.hackerrank.com/challenges/string-o-permute/problem

n을 입력받은 후 n회 입력되는 문자열에 대해서 이웃한 두 문자의 순서를 서로 바꾼다.

import Control.Monad

odd_indexed_chars = map snd . filter (odd . fst) . zip [1..]
even_indexed_chars = map snd . filter (even . fst) . zip [1..]

process cs = let l = odd_indexed_chars cs
                 r = even_indexed_chars cs
             in concat . zipWith (\x y -> [y,x]) l r

main = do
	n <- readLn :: IO Int
	forM_ [1..n] $ \_ -> getLine >>= process