(Haskell) Applicative Functor

Applicative Functor

Applicative Functor는 하스켈의 Control.Applicative 모듈에서 정의하고 있다.

하스켈의 함수는 알다시피 모두 자동으로 커리된다. a -> b -> c 타입의 함수 f가 있다고 할 때, 인자 하나를 전달한 f x의 타입은 b -> c인 함수가 된다. 함수 역시 Functor의 일종이며, 따라서 함수에 함수를 맵핑하는 것 (이는 함수 합성과 동일하다)도 가능했다. 다만 이 때의 사상되는 함수들은 모두 단인자 함수임에 주목하자.

f x = x + 1
g x = x * 2
fmap g f = g . f = (\x -> g (f x))

그런데 *처럼 인자 두 개를 받는 함수를 다른 함수에 적용하면 어떻게 될까? 예를 들어 (*) 함수를 Just 3 같은 곳에 맵핑하면 무슨 일이 벌어질까?

> :t fmap (*) (Just 3)
fmap (*) (Just 3) :: Num a => Maybe (a -> a)

Maybe a를 만드는 함수 대신에 Maybe (a -> a)를 만드는 함수가 맵핑된거랑 같은 결과가 나왔다. 이는 함수가 특정한 문맥으로 감싸진 상태를 얻은 것이다.

다른 케이스를 생각해보자. 만약 문자열(이는 [Char]타입이지?)에 compare 함수를 맵핑하는 경우도 있을 수 있다. 이 함수는 (Ord a) => a -> a -> Ordering 타입이다. 따라서 이 함수를 문자열에 맵핑해서 얻는 결과는 [(a -> Ordering)]이다. 즉 인자를 하나 받을 수 있는 함수의 리스트가 되었다.

이로써, 인자 두 개를 받는 함수를 어떤 functor에 맵핑하면, 해당 functor로 싸여진 단 인자 함수를 얻게 되는 것을 보았다. 특정 문맥에 싸여진 함수라… 이것을 어디에 활용할 수 있을까? functor 속에 있는 함수가 있기 때문에 다음과 같은 식으로 “적용하는 함수”를 다시 적용하여 값을 얻을 수 있을 것이다.

> let a = fmap (*) [1,2,3,4]
> : a
a :: [Integer -> Integer]
> fmap (\f -> f 9) a
[9, 18, 27, 36]

오, 쿨한가? 반대로 이번에는 Just (3 *)라는 함수를 얻었다고 생각해보자. 그리고 다른 하나의 값은 역시 같은 functor에 싸여있는 Just 5이다. 불행히도 이 경우에는 어느 쪽도 다른쪽에 맵핑할 수가 없다. 즉 fmap으로는 특정한 functor 내부에 있는 함수를 functor 내에 있는 값에 사상할 수 없다. 물론 컨스트럭터에 의해 패턴 매칭을 통해서 내부의 값을 추출하여 적용할 수 있지만, 우리는 좀 더 추상적이고 범용적인 방법을 원한다.

이럴 때 쓰라고 Applicative 타입 클래스가 정의되어 있다. 이 클래스의 타입은 내부에 함수 및 값을 내포할 수 있는 래퍼이다. (따라서 Applicative의 인스턴스가 되기 위해서는 Functor의 인스턴스여야 한다.) 이 클래스는 두 개의 함수, pure<*>로 구성되는데, 일단 그 정의는 다음과 같다.

class (Functor f) => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

정의 자체는 매우 간단한데, 여기서 유추할 수 있는 사실들을 정리해보자.

  1. 정의에서 알 수 있듯이 Applicative가 되기 위해서는 반드시 Functor여야 한다. 따라서 이 클래스의 인스턴스인 타입에 대해서는 fmap을 이용해서 함수를 사상할 수 있다.
  2. pure 함수는 일반적인 값 또는 함수를 해당 문맥으로 둘러싸는 작업을 한다.
  3. <*> 연산자는 감싸진 함수를 감싸진 값에 사상한다. 그 결과 앞에서 필요로 했던 문제가 해결 된다.

Maybe 타입이 이 클래스에 속할 수 있을까?

instance Applicative Maybe where
  pure = Just
  Nothing <*> _ = Nothing
  (Just f) <*> x = fmap f x

그렇다. 실제로 확인해보자.

> :m Control.Applicative
> let a = fmap (*) (Just 3)
> a <*> Just 4
Just 12
> pure (*) <*> Just 3 <*> Just 5
Just 15

<*> 연산자는 left-associative 하기 때문에 2인자 함수에 대해 pure를 적용하고 두 개의 인자를 차례로 연결하는 것도 가능하다. 또한 pure (*) <*> xfmap (*) x와 같다. 이를 좀 더 편하게 쓰게 해주는 연산자로 <$>가 있다.

(<$>) :: (Functor f) => (a -> b) -> f a -> f b
f <$> x = fmap f x

따라서 위 예제의 마지막 구문은 다음과 같이 바꿔 쓸 수 있다.

> (*) <$> Just 3 <*> Just 5
Just 15

리스트 역시 Applicative의 인스턴스이며 다음과 같은 정의를 따른다.

instance Applicative [] where
  pure x = [x]
  fs <*> xs = [f x | f <- fs, x <- xs]

정의가 매우 참신하고 간단하다!

> [(*0), (+100), (^2)] <*> [1,2,3]
[0,0,0,101,102,103,1,4,9]

아래와 같은 연결도 가능하다.

> [(+),(*)] <*> [1,2] <*> [3,4]
-- [(1+),(2+),(1*),(2*)] <*> [3,4] (정확하게 이건 아니다.)
[4,5,5,6,3,4,6,8]

IO 모나드도 마찬가지로 Applicative의 인스턴스이다. 이런 경우가 실제 적용 예에서는 얼마나 되는지 모르겠지만…

instance Applicative IO where
  pure = return
  a <*> b = do
    f <- a
    x <- b
    return (f x)

이렇게 정의된다. 이런 함수를 예로 들어보자.

myAction :: IO String
myAction = do
  a <- getLine
  b <- getLine
  return (a ++ b)

이는 다음과 같이 재정의할 수 있겠다.

myAction = (++) <$> getLine <$> getLine

따라서 두 줄을 입력받고, 이를 연결해서 출력하는 프로그램은

main = do
 a <- getLine
 b <- getLine
 putStrLn $ a ++ b

가 아니라

main = do
  a <- (++) <$> getLine <*> getLine
  putStrLn a

이렇게 할 수 있다는 의미이다.

함수에 대한 Applicative

함수 역시 functor이며, 동시에 applicative functor가 될 수 있다. functor가 함수라는 의미는 함수에 적용되는 문맥(??!!)내에 특정한 값이 들어 있다는 의미인 셈이다. 기본적으로 함수에 다른 함수를 사상하는 것은 사상 받는 함수와 사상하는 함수를 합성하는 결과를 갖는다.

fmap f g = f . g

함수가 applicative functor가 된다는 의미는 쉽게 와닿지 않는데, 어쨌든 다음과 같이 구현은 할 수 있다.

instance Applicative ((->) r) where
  pure x = (\_ -> x)
  f <*> g = \x -> f x (g x)

위 코드를 들여다보면서 좀 더 깊이 생각해보자. pure 함수를 이용해서 어떤 값을 둘러 싼다는 것의 의미는 결국 해당 값을 산출해내는 함수로 둘러싼다는 의미이다. 이는 어떤 값을 어떤 함수 (여기서 함수는 어떤 상태, 문맥으로 이해한다)로 변경한다는 뜻이다. 즉 상수값과 같은 함수는 항등함수가 된다. 즉 pure :: a -> (r -> a)로 이해할 수 있다. 이는 pure :: a -> f a의 정의와 어긋나지 않는다.

> (pure 3) "blah"
3
> pure 3 "blah"
3

이제 <*>를 살펴보자. 여기는 거의 매직이다. 클래스 정의에서의 타입 시그니처는 f (a -> b) -> f a -> f b 인데, 여기에 f(->) r 이므로, 결국 이 함수는 r -> a -> b -> r -> a -> r -> b가 된다. (뭐라고????)
즉 좌변에 2인자 함수, 우변에 1인자 함수를 받아서 다시 1인자 함수를 돌려주는데, 그 결과는 입력받은 인자를, 우변의 함수를 적용한 결과와 함께 좌변의 함수에 전달하는 결과를 얻는다.

f <*> g = \x -> f x (g x)

실제 예제를 살펴보자.

> (*) <*> (+2) $ 5
=> 5 * (5 + 2) =
35

> (+) <$> (+3) <*> <*100> $ 5
(+) (5+3) (5*100)
508

이 경우에는 인자 수만큼 연결이 가능하다. 따라서

> (\ x y z -> [x, y, z]) <$> (+3) <*> (*2) <*> (/2) $ 10
[13.0, 20.0, 5.0]

와 같은 계산도 가능해진다.

어쨌든 이 케이스는 흔히 쓰이지도 않을 뿐더러 직관적인 파악이 어려우므로 많이 쓰이지 않고 그리 중요한 파트는 아니다. 하지만 흥미로운 케이스임에는 틀림없다. 비슷한 예를 가지고 연습하다보면 applicative에 대한 직관을 키우는데 도움을 줄 수는 있을 것이다.

ZipList

한가지 더 살펴볼 Applicative의 종류는 ZipList이다. 이 타입은 Control.Applicative에 정의되어 있다.

사실 리스트는 한가지 방법 이상으로 applicative가 될 수 있다. 한 가지 방법은 이미 위에서 살펴본 것으로 <*> 함수에 함수의 리스트를 건네는 것으로 이는 뒤에 받을 인자들의 리스트들과 조합하여 가능한 모든 조합에 대해 계산하고 그 결과를 내놓는 것이다. 즉 [(1+), (2*)] <*> [3, 4]에서 (1+)는 각각 3, 4에 적용되고, 또 (2*)가 3,4에 각각 적용되어 최종 결과는 [4,5,6,8]이 된다.

하지만 다른 한가지 방법을 더 생각할 수 있다. 똑같이 “[(1+), (2)] <> [3, 4]를 적용했을 때[4, 8]이 나오는 것이다. 이는 각 리스트의 짝에게만 적용되는 것이다.ZipList a` 타입은 이런 용도로 사용된다. 그리고 이 타입에 대한 applicative 구현은 다음과 같다.

instance Applicative ZipList where
  pure x = ZipList (repeat x)
  ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)

ZipList는 리스트에 대한 newtype으로 정의되어 있고 내부의 값을 꺼내는 레코드 함수는 getZipList로 되어 있다.

newtype ZipList a = ZipList { getZipList :: [a] }

따라서 몇 가지 예를 다음과 같이 살펴볼 수 있겠다.

> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100,100]
[101, 102, 103]
> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100,..]
[101, 102, 103]
> getZipList $ (+) <$> ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2]
[5,3,3,4]
> getZipList $ (,,) <$> ZipList "dog" <*> ZipList "cat" <*> ZipList "rat"
[('d','c','r'), ('o','a','a'), ('g','t','t')]

(,,) 함수는 (\ x y z -> (x,y,z)의 의미이다.

liftA2, sequenceA

Control.Applicative 모듈에는 liftA2라는 함수가 있다. 이는 위에서 숱하게 써온 문구를 간략하게 줄여주는 함수이다.

liftA2 :: (Applicative f) -> (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

이 함수는 sequenceA 함수를 정의하는데 사용된다.

sequenceA :: (Applicative f) => [f a] -> f [a]
sequenceA = foldr (liftA2 (:)) (pure [])

이는 다양한 문맥으로 사용될 수 있다. (실은 sequence 도 거의 같은 결과를 낸다)

> sequenceA [Just 3, Just 4, Just 5]
Just [3,4,5] -- sequence와 같은 용법

> sequenceA [Just 3, Nothing, Just 4]
Nothing

> sequenceA [(+3), (*2), (+1)] 3
[6, 6, 4]

> sequenceA [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
(주어진 원소들의 곱)

이 함수는이런 문맥 외에도 이렇게도 쓰일 수 있다.

:t sequenceA [getLine, getLine, getLine]
sequenceA [getLine, getLine, getLine] :: IO [String]

따라서 문자열을 여러번 입력받아야 하는 경우에 이전에는 다음과 같은 식으로 처리했었는데…

main = do
  n <- readLn :: IO Int
  forM_ [1..n] doTest
    where
      doTest = do
        x <- getLine
        putStrLn . process $ x

이를 다음처럼 바꿀 수 있다.

main = do
  n <- readLn :: IO Int
  lines <- sequence . replicate n $ getLine
  forM_ lines (putStrLn . process)

참고로 sequence . replicate nControl.MonadreplicateM과 같다.