2. Context
Haskell의 타입 생성자는 일종의 문맥(Context)과 같이 취급할 수 있습니다. 예를 들어 Maybe 타입
생성자는 Maybe 라는 문맥과 그 문맥 속의 타입(Int, Char 등)으로 생각할 수 있죠.
Maybe
Int
Maybe Int에서 Maybe는 해당 타입에 대한 Context입니다. Maybe는 불확실성- 즉, 결과가
존재할 수도(Just Int) 그렇지 않을수도(Nothing)있다라는 부가적인 문맥 정보를 가집니다.
타입 생성자를 이런 문맥의 관점에서 봤을 때 특정 타입 생성자에 대한 타입 클래스는 그 문맥 고유의
행동을 정의하는 것으로 생각할 수 있습니다.
3. Functor
Functor 타입 클래스는 context에 대해 함수를 적용할 수 있는 타입들의 집합입니다. Functor 타입
클래스는 다음 하나의 함수 인터페이스만을 갖고 있습니다.
class Functor f where
fmap :: ( a -> b ) -> f a -> f b
Functor는 a -> b 함수를 하나 받아서, context f 내의 있는 a 타입의 원소에 그 함수를 적용시킨
결과 f b 를 만들어내는 함수 fmap을 쓸 수 있는 타입들의 집합입니다. list 역시 일종의 context로 볼
수 있고(타입 생성자이므로), 이 때 list에 대한 fmap은 우리가 계속 써왔던 map입니다.
map :: ( a -> b ) -> [a] -> [b]
list context에 대한 fmap은 해당 list 내부에 속한 원소 전체에 대해 해당 함수를 적용시키는 것이죠.
4. Functor
Maybe 타입에 대한 Functor는 다음과 같이 정의됩니다.
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
Maybe 타입은 Context 측면에서 봤을 때 불확실성(존재하는지 아닌지 - 성공했는지 실패했는지
알 수 없음)을 의미하기 때문에, 그 내부에 어떤 값도 없다면(Nothing) 결과로 Nothing을 반환하고,
그렇지 않다면 Context 내부에 속한 값(Just a에서 a)에 함수를 적용한 결과를 반환합니다.
5. Functor
Functor는 임의 Context에 대해 동작하는 함수를 만들고 싶을 때 유용합니다.
threeRepeat :: (Functor f) => f a -> f [a]
threeRepeat = fmap (replicate 3)
위와 같은 함수가 있다고 합시다. 이 함수는 인자로 넘어오는 값이 어떤 Context에 속해있느냐에 따라
다른 동작을 하게 됩니다. Context와 무관하게 유연한 동작을 할 수 있는 거죠.
Prelude> threeRepeat [1,2,3,4]
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]
Prelude> threeRepeat (Just 3)
Just [3,3,3]
6. Functor Rule
Functor 타입 클래스에 속하는 타입들은 반드시 다음 규칙을 만족해야합니다.
1. fmap id = id
id는 인자로 넘긴 값을 그냥 그대로 돌려주는 함수입니다(id x = x). fmap id는 Context 내부에
속하는 값에 대해 id 함수를 적용한다는 의미이므로, 그냥 원래 값을 그대로 돌려주는 id와 값의
차이가 없어야할 것입니다.
2. fmap (f . g) = fmap f . fmap g
이는 Context에 대해 합성함수 f . g를 적용한 것과, g, f를 순서대로 Context 내부에 mapping했을
때 결과의 차이가 없어야함을 뜻합니다.
7. Functor Rule
앞의 규칙은 Haskell 컴파일러가 알아서 잡아주지 않기 때문에, Functor 타입 클래스에 속하는
타입을 만들 때 스스로 주의해서 작성해야합니다.
저런 규칙은 얼핏 불필요하고 번거로워보일 수 있지만, 해당 타입을 쓰는 입장에서 fmap의 동작이
어떻게 될지 예측할 수 있게 해주며, fmap 함수가 정말로 Context 내부에 함수를 mapping해준다는
동작 그 자체만을 수행함을 보장해줍니다. 사용자 입장에서 fmap을 쓸 때 다른 부가적인 어떤 결과가
절대 발생하지 않을 것임을 믿고 쓸 수 있다는 뜻이죠.
8. 연습문제
아래 자료구조 Tree에 대한 Functor instance를 작성해봅시다.
data Tree a = Node a [Tree a]
소스 코드 파일은 아마 아래와 같은 형태가 되겠죠.
import Control.Functor
data Tree a = Node a [Tree a]
instance Functor Tree where
fmap ...
9. Applicative Functor
앞에서 Functor는 어떤 일반적인 값에 대한 함수 (a->b)와 Context 속에 속하는 값(f a)이 주어졌을
때 그 Context 내부 값에 함수를 적용시킨 결과 (f b)를 돌려줄 수 있는 타입들의 집합이라고
했습니다.
Applicative Functor는 여기서 한발짝 더 나아간 개념입니다. Haskell의 커링 개념을 이용해서 다음
식을 실행했을 때 결과는 어떻게 될까요?
fmap (*) (Just 3)
이 결과는 당연히 Just (*3)이 될 것입니다. 이건 Context 속에 함수가 들어가 있는 형태로 볼
수 있죠. 이 때 Just (*3)과 Just 6이 있을 때 결과를 계산할 수 있을까요? 왠지 결과로 Just 18
을 얻을 수 있어야할 것 같지 않나요? 이럴 때 쓸 수 있는 타입이 Applicative Functor에 속하는
타입들입니다.
10. Applicative Functor
Applicative Functor는 다음과 같이 정의되어 있습니다.
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Applicative 타입 클래스에 속하려면 우선 Functor 타입 클래스에 속해야합니다. Applicative가 한
발 더 나아간 개념이기 때문에 어찌보면 당연한 일이죠.
우선 pure 함수부터 살펴봅시다. pure 함수는 그냥 값이 주어져있을 때 이 값을 단순히 해당
Context 내부로 집어넣는 역할을 합니다. Maybe context에 대해 pure 3 = Just 3 이 될거라고
생각할 수 있겠죠.
Applicative Functor는 사용하려면 Control.Applicative 모듈을 임포트해야합니다
(import Control.Applicative).
11. Applicative Functor
다음은 Applicative Functor의 핵심 기능을 하는 함수인 <*> 입니다. 이 함수는 Context 내부에
있는 함수를 꺼내서, 그 함수를 Context 내부의 값에 적용시킨 결과를 반환합니다. 예를 들어 Maybe
타입에 대해 Applicative 타입 클래스는 다음과 같이 정의되어 있습니다.
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
context 속에 어떤 함수도 존재하지 않는다면 Nothing, 그렇지 않다면 그 함수를 Context 속에서
꺼내 fmap을 호출한 결과를 반환하죠.
12. Applicative Functor
Applicative Functor의 함수 pure와 <*>를 어떻게 쓰는지 예제를 살펴봅시다.
Prelude> Just (+3) <*> Just 9
Just 12
Prelude> pure (+3) <*> Just 10
Just 13
Prelude> Just (++"hahaha") <*> Nothing
Nothing
Prelude> Nothing <*> Just "Test"
Nothing
13. Applicative Functor
Haskell의 모든 함수가 다 커링이 된다는 특징 덕분에 여러 개의 인자를 가진 함수에 대해서도 <*>
함수를 적용할 수 있습니다. <*> 함수는 left-associative한 함수기 때문에 왼쪽부터 차례대로 결과를
계산하게 되죠.
Prelude> pure (+) <*> Just 3 <*> Just 5
Just 8
Prelude> pure (+) <*> Just 3 <*> Nothing
Nothing
Prelude> pure (+) <*> Nothing <*> Just 5
Nothing
14. Applicative Functor
Applicative Functor가 지켜야하는 규칙(Functor가 지켜야할 규칙과 마찬가지로 Applicative
Functor 역시 만족해야하는 규칙이 있습니다. 이후 설명)에 의해, pure f <*> x 는 fmap f x와 항상
동일합니다. 그리고 이 특징을 이용해 Applicative Functor를 좀 더 가독성 있는 방식으로 사용할 수
있습니다.
(<$>) :: (Functor f) => (a -> b) -> f a -> f b
f <$> x = fmap f x
<$>는 단순히 fmap을 중위 연산자의 형태로 표현한 함수입니다. 이 함수를 이용하면 pure f <*>
x <*> y <*> ... 형태의 식을 f <$> x <*> y <*> ... 형태로 바꿔쓸 수 있고, 이는 중간의
<$>, <*>를 제외하고 보면 일반적인 함수를 그냥 쓰는 것과 완전히 동일해 보입니다.
15. Applicative Functor
List 역시 Applicative Functor입니다. Applicative Functor로서의 List는 비결정성이라는
Context를 가지게 됩니다.
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
함수 list와 원소 list에 대해, 모든 종류의 함수 적용 조합을 다 수행한 결과를 리스트에 담고 있죠. 이는
어떤 함수를 어떤 원소에 대해 적용할지 모르는, 비결정적인 연산의 결과를 의미합니다.
16. Applicative Functor
여러 가지 예제를 살펴봅시다.
Prelude> [(*0),(+100),(^2)] <*> [1, 2, 3]
[0, 0, 0, 101, 102, 103, 1, 4, 9]
Prelude> [(+), (*)] <*> [1, 2] <*> [3,4]
[4, 5, 5, 6, 3, 4, 6, 8]
Prelude> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."]
["ha?", "ha!", "ha.", "heh?", "heh!", "heh.", "hmm?", "hmm!", "hmm."]
Prelude> (++) <$> (Just "ha") <*> (Just "!")
Just "ha!"
Applicative Functor는 위와 같이 이미 어떤 Context 내에 들어가 있는 값들에 대해 함수를
호출하고 싶을 때 유용하게 사용할 수 있습니다.
17. Applicative Functor
앞에서 언급했듯 Applicative Functor 역시 Functor와 마찬가지로 반드시 지켜야하는 규칙이
있습니다. 다 설명하자면 내용이 복잡하고 기니 설명은 생략하고, 규칙만 소개하겠습니다. 이후
Applicative Functor를 구현해야할 일이 생긴다면 그 때 깊이 있게 공부해보시는게 좋을 것 같네요.
1. pure id <*> v = v
2. pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
3. pure f <*> pure x = pure (f x)
4. u <*> pure y = pure ($ y) <*> u