Applicative functor

이번 챕터에서는 펑터에 업그레이드 버전인 Applicative 펑터에 대해서 알아보겠습니다. 하스켈에서는 Control.Applicative 모듈에 Applicative 타입클래스로 정의되어 있습니다.

하스켈에서는 기본적으로 모든 함수는 커링입니다. 이 말은 여러개의 매개변수를 받는 함수들은 사실은 하나의 매개변수를 받아서 리턴하는 함수들의 체인으로 이루어져있다는 것입니다. 만약 a -> b -> c 타입의 함수가 있다면, 두개의 매개변수를 받아서 c를 반환하는 함수라고 합니다. 하지만 실제로는 a를 받아서 b -> c를 반환하는 함수입니다. 따라서 f x y 또는 (f x) y로 함수를 호출할 수 있습니다. 이러한 매커니즘은 적은 매개변수의 함수 호출를 부분적으로 적용하여, 그 결과를 다른 함수로 전달할 수 있게 합니다.

펑터로 맵핑을 할때 보통은 하나의 매개변수만 받는 함수로 맵핑하였습니다. 하지만 만약 두개의 매개변수를 받는 * 같은 함수를 펑터로 맵핑할때는 어떻게 될까요? 예를들어 fmap (*) (Just 3)의 결과는 Just ((*) 3)이고, 이것은 Just (* 3)와 같습니다. 따라서 결과적으로 Just로 랩핑된 함수를 얻었습니다!!

ghci> :t fmap (++) (Just "hey")
fmap (++) (Just "hey") :: Maybe ([Char] -> [Char])
ghci> :t fmap compare (Just 'a')
fmap compare (Just 'a') :: Maybe (Char -> Ordering)
ghci> :t fmap compare "A LIST OF CHARS"
fmap compare "A LIST OF CHARS" :: [Char -> Ordering]
ghci> :t fmap (\x y z -> x + y / z) [3,4,5,6]
fmap (\x y z -> x + y / z) [3,4,5,6] :: (Fractional a) => [a -> a -> a]

compare의 타입은 (Ord a) => a -> a -> Ordering 입니다. 문자의 리스트를 compare로 맵핑하면 Char -> Ordering 타입의 함수의 리스트를 반환합니다. (Ord a) => a -> Ordering 함수의 리스트가 아닌 이유는 첫번째 매개변수 aChar이므로 두번째 매개변수 a는 이미 Char로 결정되었기 때문입니다.

여기서 매개변수가 여러개인 경우에는 펑터를 적용하면 함수를 가지고 있는 펑터가 된다는 것을 알았습니다. 그렇다면 이것들로 무엇을 할 수 있을까요? 우선 매개변수로 함수를 받는 함수를 맵핑할 수 있습니다. 왜냐하면 펑터안에 있는 것이 뭐든간에 매개변수로 맵핑하는 함수에 입력으로 주어지기 때문입니다.

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

만약 값이 Just (3 *)인 펑터와 값이 Just 5인 펑터를 가지고 있을때, Just (3 *)에서 함수만 꺼내서 Just 5에 맵핑하고 싶다면 어떻게 해야할까요? 일반적인 펑터는 기존 펑터에 함수를 맵핑하는 기능만 제공합니다. 내부에 함수가 포함된 펑터에서 \f -> f 9를 맵핑할때도 일반 함수로 맵핑했습니다. 따라서 fmap을 사용해서는 펑터 내부의 함수를 다른 펑터 내부의 함수로 맵핑할 수 없습니다. 펑터내의 함수를 얻기 위해서 Just 생성자에 대한 패턴 매칭을 사용할 수 있습니다. 그리고 나서 Just 5를 맵핑합니다. 하지만 이 작업을 수행하는 더 일반적이고 추상화된 방법으로 펑터를 활용할 수 있습니다.

Control.Applicative 모듈에 정의된 Applicative 타입클래스는 pure<*> 함수가 있습니다. 그리고 default 구현체는 제공하지 않습니다. 따라서 어플리케이티브 펑터로 만들기 위해서는 두 함수 모두 정의해야합니다. 이 클래스는 아래와 같이 생겼습니다.

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

이 간단한 세라인짜리 선언은 많은 것을 말해줍니다. 먼저 Applicative 클래스의 정의와 한정자를 선언하였습니다. 만약 타입생성자를 Applicative 타입클래스의 부분으로 만들려면 먼저 Functor를 가지고 있어야 합니다. 따라서 타입생성자가 Applicative 타입클래스의 부분일때는 Functor에도 포함되므로 fmap을 사용할 수 있습니다.

pure 함수의 타입은 pure :: a -> f a 입니다. f는 여기서 어플리케이티브 펑터 인스턴스입니다. pure는 임의의 타입의 값을 받아서 안에 그 값을 포함한 어플리케이티브 펑터를 반환합니다. a -> f a는 상자에 비유하면, 어떤 값을 받아서 그 결과값이 안에 들어있는 어플리케이티브 펑터 상자에 포장합니다.

pure는 어떤 값을 받아서 어떤 기본 컨텍스트안에 넣는 것 입니다.

<*> 함수의 타입은 f (a -> b) -> f a -> f b 입니다. 이것은 fmap :: (a -> b) -> f a -> f bfmap이 업그레이드된 버전입니다. fmap은 함수를 받아서 적용한 후, 펑터에 다시 넣는 반면, <*> 함수는 함수를 가진 펑터와 또다른 펑터를 받아서, 첫번째 펑터에서 함수를 빼서 두번째 펑터에 맵핑합니다. 여기서 추출한다는 것은 실제로 실행(run)하고나서 추출(extract)하는 것을 의미합니다.

Maybe 어플리케이티브 펑터

이제 MaybeApplicative의 인스턴스로 만들어 보겠습니다.

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

여기서 어플리케이티브 평터인 f는 하나의 구체적인 타입을 매개변수로 받습니다. 그래서 instance Applicative (Maybe a) where 대신, instance Applicative Maybe where로 작성되었습니다.

purepure = Just로 작성된 것은 Just와 같은 값 생성자가 일반적인 함수이기 때문입니다. 이것은 pure x = Just x와 같이 작성될 수도 있습니다.

<*>는 함수를 가지고있지않는Nothing에서 어떤 함수도 뽑아낼 수 없기때문에, 그대로 Nothing입니다. Applicative내의 클래스 선언을 보면, Functor 클래스 한정자가 있습니다. 따라서 <*>의 두개의 매개변수가 펑터라는 것을 알 수 있습니다. 따라서 첫번째 매개변수가 Just일때는 어떤 함수를 포함하게 됩니다. <*> 함수는 Just안에 포함된 함수로 두번째 매개변수인 포함된 값을 맵핑 합니다. Nothing인 경우는 어떤 함수를 맵핑시켜도 Nothing을 반환합니다.

ghci> Just (+3) <*> Just 9
Just 12
ghci> pure (+3) <*> Just 10
Just 13
ghci> pure (+3) <*> Just 9
Just 12
ghci> Just (++"hahah") <*> Nothing
Nothing
ghci> Nothing <*> Just "woot"
Nothing

이 경우, pure (+3)Just (+3)가 동일한 동작을 하는 것을 확인할 수 있습니다. 어플리케이티브 컨텍스트(예를들어 <*>)에서 Maybe 값들을 다룰때는 pure를 사용하고, 아니면 Just를 사용합니다. 위에서부터 4번째 라인까지는 단순하게 첫번째 펑터에서 함수를 추출해서 값을 맵핑하고, 다시 펑터로 랩핑하였습니다. 그리고 마지막 예제에서는 Nothing에서 함수를 추출하고 어떤값에 맵핑해서, 그 결과는 역시 Nothing이 되었습니다.

일반적인 펑터를 사용하면 함수를 펑터로 맵핑할 수 있고, 그 결과가 부분적용 함수일지라도 동일한 방법으로 그 결과를 얻을 수 있습니다. 반면에 어플리케이티브 펑터는 단일 함수로 여러개의 펑터들을 조작할 수 있습니다. 아래 코드에서 확인해보겠습니다.

ghci> pure (+) <*> Just 3 <*> Just 5
Just 8
ghci> pure (+) <*> Just 3 <*> Nothing
Nothing
ghci> pure (+) <*> Nothing <*> Just 5
Nothing

단계별로 분석해보면 아래와 같습니다.

  1. <*>는 left-associative이므로

    pure (+) <*> Just 3 <*> Just 5 -> (pure (+) <*> Just 3) <*> Just 5

  2. + 함수는 Maybe 펑터안에 포함되므로

    (pure (+) <*> Just 3) <*> Just 5 -> (Just (+) <*> Just 3) <*> Just 5

  3. (Just (+) <*> Just 3)Just (3+)이므로

    (Just (+) <*> Just 3) <*> Just 5 -> Just (3+) <*> Just 5

  4. Just (3+)도 부분적용로 커링되므로

    Just (3+) <*> Just 5 -> Just 8

어플리케이티브 펑터와 pure f <*> x <*> y <*> ...와 같은 어플리케이티브 스타일은 펑터로 쌓여있지않은 매개변수를 가진 함수를 허용하고, 이 함수가 펑터 콘텍스트내의 여러개의 값들을 조작할 수 있습니다. 그리고 이 함수는 항상 <*>에 의해서 단계별로 부분적용되기 때문에 원하는 만큼의 매개변수를 받을 수 있습니다.

pure f <*> xfmap f x와 동일한 것을 고려하면, 어플리케이티브 펑터가 훨씬 더 편리하고 분명해집니다. 이것이 어플리케이티브의 첫번째 법칙입니다. 이 부분은 뒤에서 자세히 다루겠습니다. 전에 이야기한 것처럼 pure는 기본적인 콘텍스트안에 어떤 값을 넣습니다. 함수를 넣으면 함수를 추출하여 다른 어플리케이티브 펑터에 있는 값에 적용합니다. 따라서 pure f <*> x <*> y <*> ...fmap f x <*> y <*> ...와 같습니다. 이것이 Control.Applicativefmap의 중위 연산자 버전인 <$> 함수를 제공하는 이유입니다.

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

Quick Reminder: 타입 변수를 매개변수 이름 또는 다른 값 이름과 독립적입니다. 함수 선언의 ff를 대체하는 모든 타입 생성자가 Functor 타입클래스에 있어야 한다는 클래스 제약이 있는 타입 변수 입니다. 함수 본문의 f는 맵핑하는 함수 f를 나타냅니다. 사실은 둘 모두를 표현하기 위해서 f를 사용해도 같은 것을 나타낼 수는 없습니다.

<$>를 사용하면 더 어플리케이티브 스타일로 작성할 수 있습니다. 예를들어 세개의 어플리케이티브 펑터의 f를 적용하고 싶다면 f <$> x <*> y <*> z와 같이 작성할 수 있습니다. 매개변수가 어플리케이티브 펑터가 아니라 일반적인 값인 경우, f x y z로 작성합니다.

Just "johntra"Just "volta"가 있을때는 하나의 Maybe 펑터안에 하나의 문자열로 합치고 있다면 아래와 같이 할 수 있다.

ghci> (++) <$> Just "johntra" <*> Just "volta"
Just "johntravolta"

내부적인 변환 과정은 아래 예제와 비교해보면 알 수 있습니다.

ghci> (++) "johntra" "volta"
"johntravolta"

아래와 같은 과정으로 변환되었다는 것을 알 수 있습니다.

  1. (++) <$> Just "johntra" <*> Just "volta" 실행.
  2. 타입이 (++) :: [a] -> [a] -> [a](++) 함수로 Just "johntra"가 맵핑
  3. 타입이 Maybe ([Char] -> [Char])Just ("johntra"++)로 변환
  4. Just ("johntra"++) <*> Just "volta"에서 Just가 가진 함수를 빼서 Just "valta"를 맵핑
  5. Just "johntravolta"로 변환

리스트 어플리케이티브 펑터

리스트 역시 어플리케이티브 펑터입니다. 리스트를 어플리케이티브 펑터로 인스턴스화하면 아래와 같습니다.

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

이전에도 말했듯이 pure는 어떤 값을 받아서 기본 콘텍스트안에 그 값을 넣습니다. 리스트는 최소한의 컨텍스트로 빈리스트, []가 될 수 있습니다. 따라서 빈리스트에는 pure를 사용할 수 없습니다. 이러한 이유로 pure는 한개의 값을 받아서 싱글톤 리스트에 그 값을 넣습니다. 유사하게 Maybe의 최소한의 컨텍스트는 Nothing이고, 값을 담을 수 없기때문에 pureJust로 구현되었습니다.

ghci> pure "Hey" :: [String]
["Hey"]
ghci> pure "Hey" :: Maybe String
Just "Hey"

<*>의 경우, 타입을 리스트에 한정지으면 (<*>) :: [a -> b] -> [a] -> [b]가 됩니다. 그리고 <*>는 list comprehension으로 작성되어 있습니다. <*>은 왼쪽의 매개변수에서 함수를 추출하여 오른쪽 매개변수에 맵핑합니다. 하지만 리스트의 경우는 왼쪽의 리스트가 빈리스트이거나, 하나의 함수를 포함하거나, 여러개의 함수를 가질수도 있습니다. 오른쪽의 리스트도 여러개의 값들을 가질 수 있습니다. 그래서 list comprehension으로 두개의 리스트를 모두 표현하였습니다. 왼쪽 리스트의 가능한 함수를 오른쪽 리스트의 모든 가능한 값들에 적용합니다. 즉, 왼쪽의 함수들과 오른쪽 값들의 모든 가능한 조합의 리스트를 만듭니다.

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

왼쪽에 세개의 함수와 오른쪽에 3개의 값이 있으므로 총 9개의 결과값을 가진 리스트가 반환됩니다. 만약 두개의 매개변수를 받는 함수들의 리스트를 가지고 있다면, 이 함수들에 두개의 리스트들을 적용할 수 있습니다.

ghci> [(+),(*)] <*> [1,2] <*> [3,4]
[4,5,5,6,3,4,6,8]

<*>는 left-associative이기 때문에 [(+),(*)] <*> [1,2]가 먼저 수행됩니다. 순서대로 살펴보면 아래와 같습니다.

1 [(+),(*)] <*> [1,2] -> [(1+),(2+),(1*),(2*)] 2 [(1+),(2+),(1*),(2*)] <*> [3,4] -> [4,5,5,6,3,4,6,8]

어플리케이티브 스타일로 아래와 같이 작성할 수 있습니다.

ghci> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."]
["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."]

리스트는 비결정적(non-deterministic) 계산으로 볼 수 있습니다. 100이나 "what"와 같은 값은 하나의 결과만 가지기 때문에 결정적(deterministic) 계산이라고 볼 수 있지만, [1,2,3]과 같은 리스트는 어떤 결과를 가질지를 결정할 수 없기때문에 가능한 모든 결과를 나타냅니다. 그래서 (+) <$> [1,2,3] <*> [4,5,6]를 수행할때, 두개의 비결정적 계산을 +로 합쳐서, 다른 불확실한 비결정적 계산을 만들어냅니다.

list comprehensions을 어플리케이티브 스타일을 사용하여 대체할 수 있습니다. [2,5,10][8,10,11]의 가능한 모든 곱의 리스트를 만들고 싶다면 아래와같이 작성할 수 있습니다.

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]

이것을 어플리케이티브 스타일로 바꾸면 아래와 같습니다.

ghci> (*) <$> [2,5,10] <*> [8,10,11]
[16,20,22,40,50,55,80,100,110]

여기에서 50보다 큰값만 리스트 남기고 싶다면 아래와 같이 작성합니다.

ghci> filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]
[55,80,100,110]

리스트에 대해서 pure f <*> xsfmap f xs가 어떻게 같은지 쉽게 이해할 수 있습니다. pure f[f]이고 [f] <*> xs는 왼쪽 리스트의 모든 함수로 오른쪽의 모든 값들을 맵핑할 것 입니다. 여기에 왼쪽 리스트에 한개의 함수만 있다면 맵핑과 같습니다.

IO 어플리케이티브 펑터

다른 어플리케이티브 펑터의 예로 IO가 있습니다.

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

pure는 어떤 결과를 가지고 있는 최소한의 컨텍스트에 하나의 값을 넣는것이 전부입니다. return이 정확하게 그런일을 하기때문에 여기서는 pure가 그냥 return을 하였습니다. 여기서 return은 아무것도 하지않는 I/O 작업을 만듭니다. 이것의 결과값으로 어떤 값이 나오지만 파일을 읽거나 터미널에 찍는 어떤 I/O 작업같은 것은 실제로 하지 않습니다.

<*>의 타입을 IO에 제한하면, (<*>) :: IO (a -> b) -> IO a -> IO b가 됩니다. 여기서는 IO의 결과가 함수가되는 I/O 작업과 다른 I/O 작업을 입력으로 받아서 새로운 I/O 작업을 생성합니다. a <*> b의 구현부에서는 do 구분내에서 첫번째 IO에서 함수를 얻고, 두번째 IO에서 값을 얻은 후, 함수에 적용하여 결과를 만듭니다. do 구분은 여러개의 I/O 작업들을 받아서, 하나의 작업으로 붙입니다.

Maybe[]에서 <*>는 단순히 왼쪽 매개변수에서 함수를 빼서, 오른쪽 매개변수의 값을 적용하는 함수였습니다. IO에서도 함수를 추출하고 적용하지만, 두가지 I/O 작업을 받아서 순서대로 수행하여 붙이기 때문에 순서적인 개념이 생깁니다. 따라서 반드시 첫번째 I/O 작업에서 함수를 먼저 추출해야 하는데, 함수를 추출하려면 해당 I/O 작업을 실행해야 합니다.

아래 예제를 보겠습니다.

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

사용자로부터 두개의 라인을 입력받아서, 두 라인을 합치는 함수입니다. 여기서는 두개의 getLinereturn을 하나의 I/O 작업으로 붙였습니다. 이것을 어플리케이티브 스타일로 작성하면 아래와 같습니다.

myAction :: IO String  
myAction = (++) <$> getLine <*> getLine

여기서 getLine은 I/O 작업이고, 함수의 타입은 getLine :: IO String입니다. 두개의 어플리케이브 펑터 사이에 <*>를 사용했을때, 그 결과도 역시 어플리케이티브 펑터입니다.

(++) <$> getLine <*> getLine의 타입은 IO String입니다. 따라서 이것은 어떤 값을 포함한 일반적인 I/O와 같다는 것을 알 수 있습니다. 이점을 이용하면 아래와 같이 할 수 있습니다.

main = do  
    a <- (++) <$> getLine <*> getLine  
    putStrLn $ "The two lines concatenated turn out to be: " ++ a

이와같이 I/O 작업의 일부를 이름에 바인딩한 다음에 이것을 호출하고 return을 사용하여 결과를 표시할때, 어플리케이티브 스타일을 사용하면 보다 간결하게 표현할 수 있습니다.

(->) r 어플리케이티브 펑터

또다른 Applicative의 인스턴스로 (->) r가 있습니다. 어플리케이티브 스타일에서는 거의 사용되지 않지만, 재미있는 어플리케이티브이기 때문에 한번 살펴보겠습니다.

이전 섹션에서 (->) r이 어떻게 펑터가 되는지 설명한 부분을 살펴보자.

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

pure에 의해서 어떤 값이 어플리케이티브 펑터에 들어갈때, 그 값의 결과값도 항상 동일한 값이어야 합니다. 따라서 pure 함수의 구현부는 어떤 값을 받아서 입력 매개변수를 무시하고 항상 받은 값을 그대로 반환하는 함수입니다. (->) r의 인스턴스에서 pure의 타입을 보면, pure :: a -> (r -> a)입니다.

ghci> (pure 3) "blah"
3

커링에 의해서 괄호는 아래와 같이 생략될 수 있습니다.

ghci> pure 3 "blah"
3

<*>의 구현부는 좀 어려워서 어플리케이티브 스타일로 직접 사용해보는 것이 이해하기 좋습니다.

ghci> :t (+) <$> (+3) <*> (*100)
(+) <$> (+3) <*> (*100) :: (Num a) => a -> a
ghci> (+) <$> (+3) <*> (*100) $ 5
508

두개의 어플리케이티브 펑터를 사용해서 <*>를 호출하면 결과는 어플리케이티브 펑터가 됩니다. 따라서 두개의 함수를 사용하면, 하나의 함수를 반환합니다. 여기서는 (+) <$> (+3) <*> (*100)를 실행하면 (+3)(*100)의 결과에 +를 사용한 결과를 반환하는 함수를 만듭니다. 따라서 (+) <$> (+3) <*> (*100) $ 5를 실행하면 5는 먼저 (+3)(*100)에 적용되서, 8500이 됩니다. 그리고나서 8100을 매개변수로 +가 호출되어 500이 출력됩니다.

ghci> (\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5
[8.0,10.0,2.5]

이 예제도 동일하게 (+3), (*2)(/2)의 결과들이 \x y z -> [x,y,z] 함수의 매개변수로 호출됩니다. 즉, 5는 세개의 함수에 입력으로 들어가고, \x y z -> [x, y, z]는 각 함수의 실행결과들을 받습니다.

함수들은 결국 그 자신의 실행 결과를 담은 박스로 생각할 수 있습니다. 따라서 k <$> f <*> gfg의 결과값으로 k를 호출하는 함수를 생성하는 것입니다.

예를들어, (+) <$> Just 3 <*> Just 5는 안에 값이 있거나 없을 수 있는 값을 매개변수로 +를 호출합니다. (+) <$> (+10) <*> (+5)(+10)(+5)의 미래 결과값들을 매개변수로 +를 호출합니다.

함수를 사용할때는 어플리케이티브를 사용할 일이 많진 않지만, 상당히 재미있는 부분입니다. (->) r가 어떻게 Applicative의 인스턴스로 동작하는지는 크게 중요하진 않습니다. 단지 학습을 위해서 사용해보시기 바랍니다.

ZipList 어플리케이티브 펑터

이번에는 Control.Applicative에 정의된 ZipList에 대해서 알아보겠습니다.

리스트를 어플리케이티브 펑터로 만드는 방법은 다양합니다. 그중 한가지 방법으로 함수의 리스트와 값의 리스트로 <*>를 호출하면 리스트의 모든 함수에 모든 값을 적용한, 모든 가능한 조합의 리스트가됩니다.

예를들어, [(+3),(*2)] <*> [1,2]에서 (+3)12에 모두 적용되고, (*2)도 마찬가지로 모두 적용되어 4개의 결과값을 포함한 [4,5,2,4]가 됩니다.

다른 방법으로 [(+3),(*2)] <*> [1,2]에서 첫번째 함수는 첫번째값만, 두번째 함수는 두번째값만 적용해서 [1 + 3, 2 * 2], 결과적으로 [4, 4]가되는 어플리케이티브 펑터가 바로 ZipList입니다.

하나의 타입은 동일한 타입클래스 두개의 인스턴스를 가질 수 없기때문에 ZipList의 타입은 하나의 리스트 매개변수를 받는 ZipList a 입니다. 아래 인스턴스입니다.

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

<*>는 첫번째 함수는 첫번째 값에 적용하고, 두번째 함수는 두번째 값에 적용합니다. 이와같은 작업을 zipWith (\f x -> f x) fs xs로 끝냈습니다. zipWith가 동작하는 방식에 의해서 결과 리스트의 길이는 두 리스트중 짧을 리스트와 같습니다.

pure는 어떤 값을 받아서 그 값을 무한히 반복적으로 가지고있는 리스트안에 넣습니다. 예를들어 pure "haha"ZipList (["haha", "haha", "haha"]...가 됩니다. pure 함수는 최소한의 컨텍스트에 그 값을 넣어야 하는데, 무한 리스트는 최소한이 될 수 없다고 생각할수도 있습니다. 하지만 zip 리스트는 모든 위치에 값을 만들어야 하기 때문에 의미가 있습니다. 예를들어 pure 3ZipList [3]를 반환하는데, pure (*2) <*> ZipList [1,5,10]ZipList [2]가 됩니다. 왜냐하면, 두개의 리스트중 짧은 길이의 리스트로 zip되기 때문입니다. 따라서 무한 리스트를 사용하면, 결과 리스트의 길이는 항상 무한 리스트의 길이와 같습니다. 또한 purepure f <*> xs = fmap f xs 법칙 역시 만족합니다.

이제 어플리케이티브 스타일로 zip 리스트를 사용해보겠습니다. 여기서 ZipList a 타입은 Show의 인스턴스가 아니라서, zip 리스트에서 raw 리스트를 빼는 getZipList 함수를 사용하였습니다.

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

(,,) 함수는 \x y z -> (x,y,z)와 같습니다. 또한 (,)\x y -> (x,y)와 같습니다.

zipWith외에도 표준 라이브러리에는 zipWith3, zipWith4와같은 함수가 7까지 있습니다. zipWith는 두개의 리스트를 받아서 한개로 압축합니다. zipWith3는 세개의 리스트를 받아서 하나로 압축합니다. 어플리케이티브 스타일을 사용하면 리스트의 개수마다 다른 함수를 사용할 필요가 없습니다. 임의의 리스트를 계속 추가할 수 있습니다.

liftA2 함수

Control.Applicative에는 liftA2라는 함수가 정의되어 있습니다.

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

이 함수는 특별할 것은 없지만, 두개의 어플리케이티브들 사이에 함수를 적용하여 지금까지 보아온 어플리케이티브 스타일을 숨깁니다. 이것은 어플리케이티브 펑터가 일반 펑터보다 강력한 이유를 보여줍니다. 일반 펑터를 사용하면 함수를 하나의 펑터로 맵핑할 수 있습니다. 하지만 어플리케이티브 펑터는 여러 펑터간에 함수를 적용할 수 있습니다. 결과적으로 liftA2 는 일반 바이너리 함수를 받아서, 두개의 펑터에서 작용하는 함수로 승격시켰다고 볼 수 있습니다.

우리는 두개의 어플리케이티브 펑터를 받아서 결과들을 가지고 있는 하나의 어플리케이티브 펑터안에 조합할 수 있습니다. 예를들어 Just 3Just 4를 가지고 있을때, Just [4]를 얻고 싶다면 아래와 같이 간단하게 처리할 수 있습니다.

ghci> fmap (\x -> [x]) (Just 4)
Just [4]

Just 3Just [4]를 가지고 있을때, Just [3,4]를 얻고 싶다면? 아래와 같은 방법을 사용할 수 있습니다.

ghci> liftA2 (:) (Just 3) (Just [4])
Just [3,4]
ghci> (:) <$> Just 3 <*> Just [4]
Just [3,4]

:는 어떤 구성요소와 리스트를 받아서 구성요소를 리스트에 붙인 새로운 리스트 반환하는 함수 입니다.

이제 Just [3,4]를 얻었습니다. 여기에 Just 2를 조합하면 Just [2,3,4]를 얻을 수 있습니다. 어떤 양의 어플리케이티브도 결과들의 리스트를 가진 하나의 어플리케이티브에 조합할 수 있습니다.

sequenceA 함수

sequenceA는 어플리케이티브의 리스트를 받아서 각 어플리케이티브들의 결과값들을 리스트로 가지고 있는 어플리케이티브 한개를 반환하는 함수 입니다. 이 함수는 아래와 같이 정의되어 있습니다.

sequenceA :: (Applicative f) => [f a] -> f [a]  
sequenceA [] = pure []  
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs

내부적으로 재귀가 사용되었습니다. 먼저 타입을 보면 어떤 어플리케이티브의 리스트를 리스트를 가진 어플리케이티브에 넣을 것 입니다. 빈리스트를 결과값의 리스트를 가진 어플리케이티브로 바꾸려면, 빈리스트를 기본 컨텍스트에 넣으면 됩니다. 그리고 head(어플리케이티브)와 tail(어플리케이티브 리스트)이 있는 어플리케이티브의 리스트일때는 tail 부분을 sequenceA를 재귀 호출해서 리스트를 가진 어플리케이티브로 만듭니다. 그리고 여기에 head인 x만 붙이면 됩니다.

따라서 sequenceA [Just 1, Just 2]를 실행하면 (:) <$> Just 1 <*> sequenceA [Just 2]가 됩니다. 수행과정을 보면 아래와 같습니다.

(:) <$> Just 1 <*> sequenceA [Just 2] (:) <$> Just 1 <*> ((:) <$> Just 2 <*> sequenceA []) (:) <$> Just 1 <*> ((:) <$> Just 2 <*> Just []) (:) <$> Just 1 <*> Just [2] Just [1,2]

sequenceA를 만드는 다른 방법으로 fold를 사용할 수 있습니다. fold를 사용하면 리스트의 구성별로 결과를 누적하는 대부분의 기능을 구현할 수 있습니다.

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

초기값은 pure []이고 리스트의 오른쪽에서부터 연산한 누적값을 초기값에 업데이트합니다. 누적값과 어플리케이티브 내부에 결과 리스트의 마지막 요소로 liftA2 (:)를 수행합니다. 이 과정을 누적값만 남을때까지 반복합니다.

이 함수를 실행해보면 아래와 같습니다.

ghci> sequenceA [Just 3, Just 2, Just 1]
Just [3,2,1]
ghci> sequenceA [Just 3, Nothing, Just 1]
Nothing
ghci> sequenceA [(+3),(+2),(+1)] 3
[6,5,4]
ghci> 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]]
ghci> sequenceA [[1,2,3],[4,5,6],[3,4,4],[]]
[]

sequenceAMaybe를 사용하면 리스트의 모든 결과들이 Maybe입니다. 만약 값들의 하나가 Nothing이면, 결과도 역시 Nothing입니다. 이것은 Maybe값들의 리스트를 가지고 있고, Nothing이 하나도 없는 값들에만 관심이 있을때 유용합니다.

sequenceA가 함수를 사용할때는 함수의 리스트를 받아서 리스트를 반환하는 함수를 반환합니다. 예제에서는 부분함수에 매개변수로 숫자를 넘겨서 리스트의 각 함수가 적용된 결과의 리스트를 얻었습니다. 즉, sequenceA [(+3),(+2),(+1)] 3에서 (+3) 3, (+2) 3, (+1) 3으로 적용된 값들의 리스트를 반환하였습니다.

(+) <$> (+3) <*> (*2)는 매개변수를 받는 함수를 생성합니다. 입력받은 값을 (+3)(*2) 모두에 넣고 구한 결과값을 +에 적용해서 결과값을 얻습니다. 같은 맥락에서 sequenceA [(+3),(*2)]는 어떤 매개변수를 받는 함수를 만들고, 입력값을 리스트내의 모든 함수에 넣어서 수행합니다. 함수의 결과들로 +를 호출하는 대신에 :pure []의 조합으로 함수의 결과 리스트에서 결과값들을 수집합니다.

sequenceA는 함수의 리스트를 가지고, 어떤 동일한 입력값을 모든 함수에 넣고 결과의 리스트를 보고싶을때 유용합니다. 예를들어 어떤 숫자를 가지고 리스트의 모든 조건들을 만족하는지 확인할때 사용될 수 있습니다. 유사한 동작을 아래와 같이할 수도 있습니다.

ghci> map (\f -> f 7) [(>4),(<10),odd]
[True,True,True]
ghci> and $ map (\f -> f 7) [(>4),(<10),odd]
True

여기서 and는 boolean의 리스트를 받아서 모두 TrueTrue를 반환하는 함수입니다. 그리고 동일한 동작을 위해 sequenceA를 사용하면 아래와 같습니다.

ghci> sequenceA [(>4),(<10),odd] 7
[True,True,True]
ghci> and $ sequenceA [(>4),(<10),odd] 7
True

sequenceA [(>4),(<10),odd]는 어떤 숫자를 받는 함수를 생성하고, [(>4),(<10),odd]의 모든 조건에 입력값을 넣습니다. 그 결과로 boolean의 리스트를 반환합니다. 이 과정을 타입으로 설명하면 (Num a) => [a -> Bool](Num a) => a -> [Bool]로 바꾸는 것 입니다.

리스트의 모든 구성요소들은 동일한 타입이기때문에 함수 역시 모두 동일한 타입이어야 합니다. [ord, (+3)]와 같은 리스트는 ord(+3)의 타입이 다르기 때문에 만들수 없습니다.

[]를 사용했을때, sequenceA는 리스트의 리스트를 받아서 리스트의 리스트를 반환합니다. 실제로는 리스트의 구성요소의 모든 가능한 조합들의 목록을 만듭니다. sequenceA로 한 작업을 그대로 리스트 정의(list comprehension)를 사용해서 실행해보면 아래와 같습니다.

ghci> 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]]
ghci> [[x,y] | x <- [1,2,3], y <- [4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
ghci> sequenceA [[1,2],[3,4]]
[[1,3],[1,4],[2,3],[2,4]]
ghci> [[x,y] | x <- [1,2], y <- [3,4]]
[[1,3],[1,4],[2,3],[2,4]]
ghci> [[1,2],[3,4],[5,6]]
[[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]]
ghci> [[x,y,z] | x <- [1,2], y <- [3,4], z <- [5,6]]
[[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]]

sequenceA [[1,2],[3,4]]의 실행과정을 설명하면 아래와 같습니다.

  1. sequenceA (x:xs) = (:) <$> x <*> sequenceA xs가 평가됨
  2. sequenceA xs가 평가되면, (:) <$> [1,2] <*> ((:) <$> [3,4] <*> sequenceA [])
  3. 종료조건에 의해서, (:) <$> [1,2] <*> ((:) <$> [3,4] <*> [[]])
  4. ((:) <$> [3,4] <*> [[]])가 평가되는 과정을 보면..

    4-1. 왼쪽에는 3,4가 있고, 오른쪽에는 []만 있으므로 [3:[], 4:[]]

    4-2. [[3],[4]]

  5. (:) <$> [1,2] <*> [[3],[4]]

    5-1. 왼쪽은 1,2, 오른쪽은 [3],[4]가 있으므로 [1:[3], 1:[4], 2:[3], 2:[4]]

    5-2. [[1,3],[1,4],[2,3],[2,4]

(+) <$> [1,2] <*> [4,5,6][1,2]에서 모든 값을 취한 x4,5,6에서 모든 값을 취한 y에 대해서 비결정적 연산 x + y의 모든 가능한 결과들을 리스트에 담습니다. 유사하게 sequence [[1,2],[3,4],[5,6],[7,8]]의 결과는 [x,y,z,w]의 비결정적 연산입니다. 비결정적 연산의 결과는 리스트로 표현합니다. 따라서 가능한 모든 결과의 리스트의 리스트로 표현합니다.

I/O 작업을 할때, sequenceAsequence와 동일합니다. I/O 작업의 리스트를 받아서 각 작업들을 수행할 I/O 작업을 반환합니다. 그리고 I/O 작업들의 결과의 리스트를 반환합니다. 따라서 [IO a]IO [a]됩니다. 모든 I/O 작업들은 순서대로 하나씩 수행되어야 합니다. 그리고 실제로 실행하지 않으면 값을 가져올 수 없습니다.

ghci> sequenceA [getLine, getLine, getLine]
heyh  
ho  
woo  
["heyh","ho","woo"]

일반적인 펑터와 마찬가지로 어플리케이티브 펑터에는 몇가지 법칙이 있습니다. 가장 중요한 것은 이미 언급한, pure f <*> x = fmap f x 입니다. 실제로 본 챕터의 예제 펑터들로 이 법칙을 증명할 수 있습니다. 다른 펑터의 법칙들로는 아래와 같은 것들이 있습니다.

  • pure id <*> v = v
  • pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
  • pure f <*> pure x = pure (f x)
  • u <*> pure y = pure ($ y) <*> u

여기서는 위 법칙들에 대해서 자세히 다루지는 않을 것 입니다. 하지만 챕터를 끝내고 자세히 살펴보면 각 법칙의 몇가지 예들을 찾을 수 있을 것 입니다.

결론적으로 어플리케이티브 펑터들은 어플리케이티브 스타일로 I/O 연산, 비결정적 연산, 실패를 가질 수 있는 연산 등, 서로 다른 연산들을 합칠 수 있다는 점에서 상당히 재미있고 유용합니다. <$><*>를 사용해서 항상 동일하게 동작하는 일반 함수들을 임의의 개수의 어플리케이티브 펑터위에서 사용할 수 있습니다.

results matching ""

    No results matching ""