컨텍스트, Applicative 펑터, Traversable

Posted on July 2, 2020

2025.1에 내용을 추가하면서 펑크터를 펑터로 바꿨습니다.

컨텍스트

맥락같이 추상적인 말 말고, 실제 코드에서 드러나는 모양을 보면, 어떤 동작을 할 때 반드시 실행되는 (주로 눈에 잘 안보이는)코드를 말합니다. 같은 타입의 동작을 여러번 연결 하면 여러 번 실행됩니다. 보통 효과가 극적으로 나타나는 건 이렇게 연결, 연결할 때입니다. 타입과 잘 설계된 구조로 프로그래머 눈에 잘 안띄게 돌아가는 경우가 많습니다.

텍스트컨텍스트
해석이 필요한 대상이면 무엇이든 텍스트text라 부를 수 있습니다. 흔히 글, 문장같은 글자로 이루어진 것들만 텍스트라 부르는데, 의미를 확장해서 보면 그림이든, 건축이든, 뭐든 해석이 가능한, 혹은 필요한 것들을 텍스트라 부를 수 있습니다.
컨텍스트 con-text는 텍스트를 해석하는데 도움을 주거나, 혹은 필요한 추가 정보 역할을 하는 것들을 말합니다. 그래서 con(함께정도의 뜻)을 붙여 컨텍스트라 부릅니다.

예를 들어, 그림 전시에서 그림을 볼 때, 그림은 텍스트고, 옆에 붙어 있는 그림 설명은 컨텍스트라 볼 수 있습니다.

Some a라는 펑터를 선언하면, a -> b 함수를 적용할 수 있는 방법을 fmap에 정의해 둡니다. a -> bSome a -> Some b로 변환한다고 말해도 되고, a -> b 함수가 a에 도달하는 방법을 정의해 두었다고 말할 수도 있습니다. Some a 타입 값을 가지고 작업할 때는, a에 도달하는 fmap이 어디서든 동작해야 합니다. 이를 Some 타입의 컨텍스트(혹은 컨텍스트에서 할 일)라 부릅니다.

하스켈에선 하나의 함수를 실행하고 나서 자동으로 다음 함수를 실행하거나 하지 않습니다. 다음 실행할 함수를 명시적으로 지정해줘야 합니다. 보통 고차 함수로 되어 있는 컴비네이터를 이용해 함수를 합성합니다. 가장 흔히 보이는 게 바로 (.) f g = \x -> g(f(x))입니다. f를 실행하고, 그 후 g를 실행하려면 g . f라고 씁니다.

그런데, fg를 합성할 때 항상 해야되는 작업이 있을 경우, (.)가 아닌 다른 걸 정의합니다. 예를 들어 특정 타입 Some을 합성할 때는, 해당 타입을 위해서만 (∘)을 정의해서 합성할 수 있습니다. 그럼, 프로그래머는 매 번 특정 작업을 해야한다는 걸 명시적으로 지정하지 않아도 (∘)으로 합성하기만 하면 알아서 작업합니다. 이럴 때, Some 타입 컨텍스트를 유지하며 작업한다, 혹은 Some 타입 컨텍스트 안에서 동작한다라고 말합니다. (∘)이 해야 할 일은 보통 Some a에서 a에 접근하기 위해 필요한 일을 합니다. 바로 Somefmap을 부르는 일입니다.

(+1)  $       2    :: Int
(+1) <$> Just 2    :: Just Int

첫 째 줄은 특별한 작업 없이 2에 도달해서 (+1)을 합니다.
두 째 줄의 <$>2에 도달하기 위해선 Maybe 펑터의 작업(Just a인지 Nothing인지 확인하는 절차)을 해야 합니다.
<$>fmap을 중위infix 형태로 바꾼 것입니다. $를 쓰지 않고 <$>를 쓰는 것만으로 Maybe의 컨텍스트는 유지됩니다.

하스켈은 인자로 쓰인 Just를 보고, 알아서 Maybe<$>를 추론, 선택합니다. 프로그래머가 명시적으로 “Maybefmap을 써”라고 알려주지 않아도 됩니다. 두 째 줄의 결과 타입은 Maybe입니다. 타입으로 분명하게 컨텍스트가 있음을 나타냅니다. 펑터 타입 자체가 컨텍스트가 있음을 표시합니다.

(+1) <$> [1,2]      :: [Int]

위와 같은 <$>를 썼지만, 이 번엔 리스트의 fmap으로 추론합니다.

(+1) <$> f 2        :: f Int

아직 어떤 인스턴스의 <$>를 쓸지 알 수 없습니다. f가 결정되면, f<$>를 가져옵니다. 즉, 컨텍스트가 결정되면, 컨텍스트에 따라 <$>가 결정됩니다. 위 코드가 만일 리스트 컨텍스트에서 놓이면, 리스트의 <$>를 가져 오고, Maybe 컨텍스트에 놓이면 Maybe<$>를 가져 옵니다. 하스켈은 타입 클래스를 이용해 컨텍스트에 따라 인스턴스를 고를 수 있습니다.

펑터의 컨텍스트는 펑터 타입을 다룰 때는 항상 동작할 수 밖에 없는 작업을 의미합니다. 이 컨텍스트에서 할 일은 펑터의 fmap에 정의해 둡니다. 명확히 얘기하면, 이 특정 작업 자체가 컨텍스트가 아니라, “이런 저런 추가 작업을 항상 해야 한다”는 컨텍스트를 가지고 있다라 말해야 하지만, 할 일 자체를 컨텍스트라 부르는 경우가 많습니다.

모나드는 컨텍스트로 이펙트 작업(함수 체인을 하는 동안 이펙트를 잘 유통시키는 작업)을 한다고 말합니다. 예를 들면, Maybe모나드는 컨텍스트에서, 펑터 컨텍스트에서 봤던 Just인지 Nothing인지 확인하는 작업(다르게 얘기하면, 실패할 수도 있다는 이펙트)이, 체이닝 동안 여러번 겹치는 것을 어떻게 처리할지를 결정하는 작업을 컨텍스트로 가지고 있습니다.

하스켈은 타입 클래스를 이용해서 모노이드니, 모나드니 하는 특정 구조들을 만들고, 실 구현 코드는 클래스의 인스턴스에 따라 골라오는 작업을 합니다. 함수(타입 클래스에서 명시한 메소드)를 체이닝해서 코드 덩어리를 만들어 놓고, 나중에 해당 코드를 쓰는 곳이 어떤 컨텍스트에 놓여 있냐에 따라 동작이 결정되는 형태를 많이 씁니다. Monad m => m a 라고 되어 있으면 나중에 m을 어떤 모나드로 추론하냐에 따라 코드 덩어리에서 쓴 메소드들의 구현체가 한 번에 달라집니다. 단순화 시켜 얘기하면, m1 >=> m2 >=> m3 ... 로 코드 덩어리를 만들고, 어떤 >=>를 쓸 건지는 나중에 결정하니, 이럴 때는 이란 개념이 직관적으로 맞아 떨어집니다.

위에 모두 컨텍스트라 썼는데, 같은 듯 다른 듯 보일 수 있습니다. 항상 맥락으로 번역하면 덜 직관적인 경우도 있는데, 메인 텍스트를 해석하는데 필요한 추가적인 정보로 바라 본다면, “컨텍스트에 따라 컨텍스트 뜻은 달라지지 않는다고 볼 수도 있습니다.” 추가적인 정보를 좀 더 구체화 하자면, 추가적인 정보가 현재 텍스트에만 필요하다면, 굳이 분리하지 않고, 텍스트에 합쳤을 겁니다. 여러 텍스트들에 같이 적용할 수 있는 정보로, 현재 텍스트에도 적용되는 것들을 컨텍스트라 부른다고 상상하고 있습니다.

컨텍스트란 용어는 엄격하게 정의된 기술 용어는 아니니, 때론 혼란을 줄 때도 있습니다만, 위와 같이 곱씹으면, 텍스트 주변에 흐르고 있는 정보로 제일 적당한 번역어는 역시 맥락 아닐까 싶습니다.

Context 맥락

Applicative functor

class (Functor f) => Applicative f where  
    pure :: a -> f a   -- f는 펑터이므로 * -> * 카인드를 받는다.
    (<*>) :: f (a -> b) -> f a -> f b 

instance Applicative Maybe where  
    pure = Just  
    Nothing <*> _ = Nothing  -- 여기 패턴 매칭에서 분기가 만들어진다.
    (Just f) <*> something = fmap f something 
        -- 첫 번째 인자를 패턴 매칭해서 분기를 만들고,
        -- something에 fmap이 실행되어 또 한 번 분기가 만들어진다.

펑터들의 컨텍스트는(컨텍스트에서 할 일은), 흔히 볼 수 있는 작업 모양 중 하나가 분기입니다. <*> 정의를 보면, NothingJust f 패턴 매칭으로 갈래 길이 드러납니다. 컨텍스트로 반복해서 동작할 코드가 바로 이 패턴 매칭입니다. (생각하기 편하게 눈에 잘 띄어 골랐습니다.)

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

보통 펑터 안에 있는 값에, 여러 개의 매개 변수를 가진 함수를 적용할 때, 커링 진행 중간 단계를 보면, 커링된 함수가 펑터 안에 놓여 있는 상태가 되어, 펑터 안에 있는 커링된 함수를 다시 접근할 때 <*> 가 필요합니다. 좀 더 쉽게 보기 위해 위 코드의 중간 커링 단계를 보면

pure (+) <*> Just 3 -- 결과는 함수가 펑터 안에 놓인 Just (3+) 상태가 됩니다.

그럼 여기서 다시 <*> 를 적용하게 되면 컨텍스트에 있는 코드가 또 동작합니다.

Just (3+) <*> Just 5 

를 만나면, 적용할 함수가 Just안에 있는지 Nothing인지 보는 패턴 매칭(Applicative의 컨텍스트)이 동작해서 (3+)를 얻고, 뒤에 Just 5(3+)를 적용하기 위해 Myabe 펑터의 fmap(Applicative에 포함되어 있는 Functor의 컨텍스트)이 동작합니다. 다시 정리해서 보면 <*>를 적용할 때마다 패턴 매칭(컨텍스트)이 계속 실행됩니다.

보통 펑터에 들어 있는 값들을 데이터 생성자에 넣어 줄 때 <*>를 사용합니다.

> data Some = Some Int Int Int deriving Show
> Some <$> Just 1 <*> Just 2 <*> Just 3
Just (Some 1 2 3)

추가 정보를 심어 놓는 형식을 컨텍스트라 부르기도 하고, 추가 정보 자체를 컨텍스트라 부르기도 합니다.

Traversable

한 발자국 더 들어가 봅시다.
maptraverse의 차이가 뭘까요?
※ 하스켈의 작명 센스는 프로그래머와 그리 친해보이지 않는다는 의견이 종종 보이는데, traverse도 단어 뜻(가로지르다. 횡단하다.)만 보고 언뜻 알기는 어려워 보입니다.

-- Traversable Maybe 인스턴스의 traverse 정의
-- <$>는 fmap의 중위infix 표현입니다.
traverse f (Just a) = Just <$> f a -- 또는 pure (Just) <*> f a
traverse f Nothing = pure Nothing

-- map 정의
map f (Just a) = Just $ f a
map f Nothing = Nothing

둘 다 펑터 안의 값에 함수를 적용하는 역할을 합니다. 단, 적용할 때 map은 기본 함수 적용이고, traverse<*>로 함수를 적용합니다. 무슨 차이가 있을까요? 이제 컨텍스트 개념을 알게 됐으니 간단히 이렇게 얘기하면 됩니다.

map은 컨텍스트 없이 함수를 적용하고, traverse는 Applicative의 컨텍스트를 발현 시키고, (혹은 컨텍스트 안에서, 혹은 컨텍스트를 유지하며) 함수를 적용한다.”

Applicative 컨텍스트로 돌아갈 코드는, 타입에 따라 정의한 인스턴스에 있는 <*>의 구현체입니다

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

리스트의 <*> 는 리스트의 comprehension 표기로 되어 있는데, f <- fs 가 동작할 때 컨텍스트가 동작하고, x <- xs 에서도 컨텍스트가 동작합니다.

class (Functor t, Foldable t) => Traversable t where

instance Traversable [] where
    traverse _ [] = pure []
    -- traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
    traverse f (x:xs) = (:) <$> f x <*> traverse f xs 
    -- f는 펑터 결과를 주는 함수이므로, <$>, <*>는 f펑터의 것을 쓴다.

    -- Base에는 liftA2로 정의되어 있다. 
    -- traverse f = List.foldr cons_f (pure [])
    --  where cons_f x ys = liftA2 (:) (f x) ys
infixl 4 <*>
infixl 4 <$>

liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 f x = (<*>) (fmap f x)

리스트에 있는 모든 엘리먼트에 f를 적용하도록 되어 있는데, 엘리먼트 하나에 적용 후 (:)로 나머지와 붙일 때 <*>가 동작하고 있습니다. f의 결과 타입이 가진 컨텍스트가 드러납니다.

liftA2를 이용한 Traversable 리스트 인스턴스는 좀 복잡해 보이는데, liftA2를 따라가면 fmap이 나옵니다. 리스트용 traversefmap으로 갈래 길을 만들고 있습니다.

> let evenFilter = (\a -> if even a then Just (a*a) else Nothing)
> map evenFilter [2,4,6] -- [Just 4,Just 16,Just 36]
> map evenFilter [1,2,3] -- [Nothing,Just 4,Nothing]
> traverse evenFilter [2,4,6] -- Just [4,16,36]
> traverse evenFilter [1,2,3] -- Nothing

traverse는 컨텍스트가 계속 동작하면서 함수를 적용합니다. mapNothing이 나오든 말든 리스트의 끝까지 함수 적용을 합니다. 하지만 traverse<*>의 패턴 매칭으로 Nothing을 만나면 더 이상 다른 <*>의 두 번째 인자에 도달하지 않고 그냥 Nothing을 반환합니다.

evenFilter를 리스트의 1에 적용 후 나머지 리스트와 붙일때 (:)<*>로 적용합니다. 인스턴스 정의의 f자리에 Maybe가 들어왔으므로 <*>는 리스트가 아니라 Maybe용이 작동합니다. 결과가 Nothing이라면 함수 적용을 하지 않고 그냥 Nothing을 반환합니다.

Nothing <*> traverse f xs 

Maybe용 인스턴스의 <*> 정의대로 위 결과값은 오른 쪽 traverse... 를 실행할 필요 없이 그냥 Nothing입니다.

그래서 traverse로 적용할 때는 하나라도 결과가 Nothing이면 전체 결과값이 Nothing입니다.

조금 복잡하긴 한데, “<*>로 적용한다”란 말은 “컨텍스트 코드를 동작시킨 후 적용하겠군” 이라고 읽으면 됩니다. evenFilter를 먹인 후 값들을 묶어 “리스트로 만들려고 할 때 Maybe의 컨텍스트가 동작한다”가 map과는 다른 traverse의 동작입니다.

단순 연결만 하려면 함수 컴포지션 (.)을 쓰면 되고, 컴포지션할 때 어떤 동작을 항상 하게 하려면 (.)를 새로 만들면 됩니다. 단순 인자를 넘길 땐 (공백)을 쓰면 되지만, 인자를 넘길 때 어떤 동작(펑터에서 꺼낸다거나…)을 항상 하게 하려면 apply를 새로 만들면 됩니다.

그리고, map은 펑터가 하나 등장하고 Traversable엔 펑터가 두 개가 등장합니다.

당연한 얘긴데 <*>로 묶으면 <*>의 코드가 실행됩니다.

2021.7.31 추가

import Control.Concurrent.STM

tv1 :: TVar Int -> STM ()
tv1 t = writeTVar t 1

tv2 :: TVar Int -> STM ()
tv2 t = writeTVar t 5

main = do
  a <- atomically $ newTVar 1
  let xs = [tv1, tv2]
  atomically $ map (\f -> f a) xs -- (가)
  r <- readTVarIO a
  print r

위 코드는 리스트가 바깥이라 atomically [STM.., STM..] 이런 모양이 됩니다. 리스트에 atomically를 적용하는 모양이니 당연히 에러입니다.

Couldn't match expected typeSTM a0’ with actual type ‘[STM ()]’

아래와 같이 바꿔 쓰면 어떻게 될까요?

map (\f -> atomically $ f a) xs

이 것도 역시 가장 바깥이 리스트 상태 [IO.., IO..]여서 IO 컨텍스트인 main에서 바로 쓰지 못합니다.

Couldn't match type ‘[]’ with ‘IO
Expected type: IO (IO ())
Actual type: [IO ()]

리스트 안에 들어 있는 IOSTM을 바깥으로 꺼내놔야 현재 컨텍스트에서 쓸 수 있습니다. 바로 이럴 때 traverse를 씁니다.

atomically $ traverse (\f -> f a) xs

다시 한번 위에 나왔던 Maybe의 인스턴스를 예로 보면, 펑터 두 개가 섞여 있는 코드에서 펑터 위치를 바꾸는 걸 알 수 있습니다.

-- f와 Just의 감싼 순서를 바꾸고 있습니다.
traverse f (Just a) = Just <$> f a

생각을 좇아가는 팁은, 패턴 매칭으로 펑터가 벗겨지는 걸 상상하며 따라가면 좋습니다. 언제나, 펑터가 가진 고유의 동작이 드러나려면 패턴 매칭으로 벗겨야 한다는 걸 기억하세요.

> :t [putStrLn "1", putStrLn "2"]
[putStrLn "1", putStrLn "2"] :: [IO ()]

-- IO와 [] 순서를 바꾸기 위해 
> :t traverse id [putStrLn "1", putStrLn "2"]
traverse id [putStrLn "1", putStrLn "2"] :: IO [()]

> traverse id [putStrLn "1", putStrLn "2"]
1
2
[(),()]

실용 프로그램을 짜다 보면 [STM], [IO]를 갖고 작업할 일이 자주 생깁니다. 리스트 안에 STM이나 IO가 있으면 traverse를 떠올리세요.

import Control.Concurrent.STM

bs :: IO [TVar String]
bs = do
    b1 <- newTVarIO "a" 
    b2 <- newTVarIO "bb"
    b3 <- newTVarIO "ccc"
    return [b1,b2,b3]

getSize :: TVar String -> IO Int
getSize b = do
    bytes <- readTVarIO b
    return $ length bytes

main = do
    bslist <- bs --[TVar..., TVar...,... ]
    sizes <- traverse getSize bslist
-- TVar 안에 들어 있는 문자열의 길이를 모두 합하려면 traverse로 돌려야 합니다.
    putStrLn $ show $ sum sizes

Q. Applicative 펑터와 모나드의 차이
Applicative와 모나드 둘 다 effect를 가지고 있습니다.
Applicative<*>를 통해 함수를 적용하는데, 모두 독립적으로 적용합니다.
이전 작업 결과(effect)가 다음 작업에 영향을 주지 않습니다. <*> 작업들은 parallel 하게 돌릴 수 있습니다.
모나드는 이전 effect와 다음 effect를 합성해서 하나의 effect로 표현합니다.
2022.6 새로 올린 Applicative Functor와 Monad 차이글을 참고하세요.

모나드는 모나드, 같음 글을 참고하세요.

traverse를 말로 설명하면

2025.1 추가
Traversable 동작은 아는데, 직관적인 느낌이 덜 와서 고민 중이었습니다. 펑터를 정리하면서 컨텍스트가 조금 더 명확해져 설명을 추가합니다.

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

타입을 보고 읽으면, 이미 존재하는 t컨텍스트에 있는 값a에 접근해 f컨텍스트를 만들어내는 함수를 적용한 후, f컨텍스트 안에서 t컨텍스트를 만든다.

[0,1,2]\x -> if x == 0 then Nothing else Just (1/x) 함수를 traverse로 적용하면 최종 결과는 Nothing입니다. 왜 이런 결과를 원할까요? 리스트는 모든 원소에 함수를 적용하는 컨텍스트를 가지고 있습니다. 리스트의 “역수를 만들어라”는 두 가지 해석이 가능합니다.

“모든 원소의 역수를 만들어라.”

  1. 만일 하나라도 역수를 만들지 못한다면 미션 실패입니다.
  2. 혹, 만들지 못하는 것들이 있으면 빼고 만들어라.

리스트 전체를 “하나”로 볼 것인가, 리스트 속에 있는 개 별 하나를 “여러번” 반복해서 바라 볼 것인가?

Just  $  [1]은 Just [1]이고,
Just <$> [1]은 [Just 1]입니다.

fmap     Just [1,2,3]은 [Just 1, Just 2, Just3]
traverse Just [1,2,3]은 Just [1,2,3]

fmap은 원래 있던 펑터 구조 안에 있는 값에, 새로운 펑터 구조를 만들어내는 함수를 적용하면, 기존 펑터 구조 안에 새 펑터가 놓이게 됩니다. 반대로 traverse는 새 펑터안에 기존 펑터가 놓이게 됩니다. 하지만, 기존 펑터 구조를 그대로 새 펑터 안으로 가져 가는 것이 아니라, 새 펑터 구조를 반영하며 다시 기존 펑터 구조를 만들어 냅니다. 쓰면서도 말이 어렵습니다. 기존 펑터를 리스트, 새 펑터를 Maybe로 넣어서 읽어 보면,

리스트 구조를 그대로 Maybe 안으로 가져 가는 것이 아니라, 리스트 원소들을 대상으로 각 각 Maybe 구조를 만들고, Maybe 구조 안에서 리스트를 다시 만듭니다.

traverse f (x:xs) = (:) <$> f x <*> traverse f xs 

원소 하나 x에 접근해 Just를 적용하면, 새로운 컨텍스트에 놓인 Just x가 만들어지고, 이 안에 있는 xfmap으로 (:) ...을 적용합니다. 이렇게 (:)을 그냥 적용하지 않고, 컨텍스트가 살아 있는 <$><*>을 쓰므로, 만일 한 번이라도 Nothing을 만나면, 더 이상 진행하지 않고 결과값은 Nothing이 됩니다.

<$><*>는 리스트 관련 인스턴스가 아니라 Applicative functor fFunctor,Traversable 인스턴스에 있는 함수(메소드)들입니다. 리스트의 Traversable 인스턴스는 외부에서 받은 f를 원소 각 각에 적용하는데,기존 리스트를 그대로 유지하는 것이 아니라 f 결과 타입의 <$>, <*>를 이용해서 다시 리스트를 만들어 냅니다.

traverse 정의를 보면, 새로운 컨텍스트 f 안에서 (:)를 써서 리스트를 만드는 걸 볼 수 있습니다.

컨텍스트란 a값에 도달하려면 반드시 거쳐야 하는 절차로 볼 수 있습니다. 리스트 컨텍스트에 따라 각 원소들에 접근하며 새로운 컨텍스트를 만들어내고, 이 새로운 컨텍스트 안에서 다시 리스트 컨텍스트를 만들어 냅니다.

리스트에 한정된 건 아니니, 좀 더 범용적으로 말을 바꾸면,

기존 컨텍스트에 있는 값에 새로운 컨텍스트를 만들어내는 함수를 적용하고, 새로 생긴 컨텍스트 안에서 다시 기존 컨텍스트를 만드는 작업을 합니다

아직 위에 정리한 문장들이 마음에 들지 않는다면, 명확히 필요한 때를 떠올려 보면 도움이 될 수 있습니다. 트리나 리스트 같은 여러 원소를 가진 것들(Traversable 인스턴스)에는, 함수를 적용하려면 각 각의 원소들에 모두 적용한다는 컨텍스트를 가지고 있습니다. 이런 컨텍스트 안에 있는 각 각의 원소들에 Maybe, IO, STM 같은 새로운 컨텍스트를 만들어내는 함수를 적용해서, Maybe, IO, STM 컨텍스트 안에서 다시 리스트를 만들어 냅니다.

[1,2,3]이라는 입력값을 가져와, 이들 각 각을 가져와서 출력하는 IO액션 리스트 [print 1, print 2, print 3]를 원할 수도 있고, print 1, print 2, print 3 각 각의 IO액션이 (IO 컨텍스트 안에서) 동작하길 원할 수도 있습니다.

[1,2,3]이라는 입력값을 가져와 evenFilter :: Int -> Maybe Int를 적용해서 [Nothing, Just 2, Nothing]이라는 결과를 만들길 원할 수도 있고, 하나라도 Nothing이면 전체 결과가 Nothing이고, [2,4,6] 같은 입력값을 가져왔다면 Just [2,4,6]이 되는 결과를 원할 수도 있습니다.

리스트 컨텍스트(함수를 각 원소마다 적용한다)에 있는 1,2,3Maybe 컨텍스트(Just인지 Nothing인지 확인한다)를 가진 결과를 만들어내는 함수를 적용한 후, 흩어져 있는 Maybe 결과값들을 다시 리스트로 모읍니다. 단, 그냥 모으는 게 아니라, Maybe 컨텍스트 안에서 리스트를 만듭니다.

두 컨텍스트를 모두 반영해서 작업한다는 말은 무슨 말일까요?

traversefmap으로 적용 후 sequenceA를 쓰는 것과 같습니다.

> sequenceA $ fmap evenFilter [2,4,6]
Just [2,4,6]
> sequenceA $ fmap evenFilter [2,4,5]
Nothing

두 개의 컨텍스트가 어떻게 돌고 있는지 읽어 낼 줄 알아야 합니다.

모든 펑터들이 쓸모 있는 Traversable 인스턴스를 가지고 있는 것은 아니고, 리스트나 트리 같은 여러 원소를 가진 펑터들이 주로 가지고 있습니다. 이 펑터들 안에 있는 값에 접근해 새로운 펑터(생성 함수)를 적용한 후 다시 모아놓을 때, 필요할 때가 있습니다.

아직도, 개념이 뭔가를 (대지를) 가로지른다는 느낌은 별로 없습니다만, 전체를 탐색하며 지나간다는 의미여서 traverse란 이름을 붙였다 보면, 그럭 저럭 수긍이 갑니다.

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com