어원
co- “together, with” - Etymonline
contra- “against, in oppsition” - Etymonline
순방향, 역방향
같은 방향으로, 반대 방향으로
등을 의미할 때 주로 쓰입니다.
무언가 기준이 있고, 그 것의 움직임, 변화에 따라 다른 것이 어떻게 되냐에 따라 붙는 용어입니다.
어원은 “tending to change”인데, 변하려는 성향정도 번역할 수 있을까요? 자료들을 보면 “변성”이라고 번역하기도 합니다. 위 접두어와 합치면, 같은 방향으로 변하면 공변covariant, 반대 방향으로 변하면 반변contravariant입니다. 반변은 반공변으로 번역하기도 합니다.
웹에서 Covariant, Contravariant를 검색하면, 수학쪽 용어도 같이 검색됩니다. 예를 들어 기저 벡터를 phi만큼 회전하면, 좌표 변환은 -phi만큼 돌린 것과 같습니다. 이럴 때 반대로Contra 변성variant, Contravariant 합니다.
※ P
: Parent 의미, C
: Child 의미
아래와 같은 상황을 가정하면
P
는 a
, b
, c
메소드를 가지고 있고,
C
는 P
가 할 수 있는 a
, b
, c
는 모두 그대로 가지고 있고, d
, f
메소드도 있다.
이럴 때 P
가 필요한 자리에, C
를 넣을 수 있습니다. P
타입을 받는다는 건, a
, b
, c
메소드를 쓰겠다는 건데, C
타입도 이들 메소드를 모두 가지고 있으니 문제가 생기지 않습니다.
P(super) <- C(sub)
관계가 있을 때, P
와 C
에 의존하는 어떤 Generic 클래스, 타입 등이
SomeVariant P <- SomeVariant C
와 같이 변성(변환) 전의 것과 같은 방향의 관계가 있을 때는 Covariant, 반대일 때는 Contravariant라 합니다.
위에 예를 든 것처럼, 다른 곳에서도 이 용어는 많이 쓰입니다. 여기서는 하스켈에서 타입 얘기를 할 때 나오는 뜻만 보겠습니다. 적어도 두 가지가 변화하는 게 있어야, 이 용어를 쓸텐데, 무엇이 변화할 때 무엇이 co, contra란 말 없이 그냥 covariant와 contravariant로 설명하는 경우가 있어, 금방 이해가 가지 않을 때가 있습니다.
하스켈에선 Functor로 타입이 변환 되었을 때, 기존 관계(함수)가 어떻게 되느냐에 관한 용어입니다.
P
, C
타입이 있고, f :: P -> C
란 함수가 있을 때 SomeFunctor
로 변환한다면,
SomeFunctor P
, SomeFunctor C
로 변환하고, 함수가
f 변환 :: SomeFunctor P -> SomeFunctor C
이면 Covariant 이고,
f 변환 :: SomeFunctor P <- SomeFunctor C
이면 Contravariant 라 합니다.
fmap :: (a -> b) -> Maybe a -> Maybe b
이 fmap
은 a -> b
를 Maybe
로 변환하면 Maybe a -> Maybe b
로 만들기 때문에 Covariant functor라고 부릅니다. 예를 들면, Int -> String
함수를 covariant functor로 변환해서 Maybe Int
에 적용할 수 있습니다. 여기까진 자주 보던 것으로 그리 낯설지 않습니다. 그런데, Int
가 출력에 있는 Double -> Int
같은 함수는 Maybe Int
에 적용할 수 없습니다.
만일, a -> b
가 Some b -> Some a
로 변환되면 Some
은 a
에 Contravariant라 말하는데, 다음에서 예시를 보겠습니다.
FPcomplete Covariance and Contravariance 에서 발췌
newtype MakeString a = MakeString { makeString :: a -> String }
showInt :: MakeString Int
showInt = MakeString show
-- (a -> b)가 아니라 (b -> a)입니다.
mapMakeString :: (b -> a) -> MakeString a -> MakeString b
MakeString g) = MakeString (g . f)
mapMakeString f (
plus3ShowInt :: MakeString Int
= mapMakeString (+3) showInt
plus3ShowInt
main :: IO ()
= putStrLn $ makeString plus3ShowInt 5 main
makeString
함수는 어떤 타입이든 받아서 String
타입을 돌려주는 함수입니다. 예를 들면 타입에 따라 타입 이름을 돌려 준다든지 하는 함수일 수 있습니다. 일단 시작부터 의도가 보입니다. a
가 출력에 있지 않고, 입력에 있습니다. 이게 fmap
을 구현할 때 어떤 영향을 미치는지 보겠습니다.
showInt
는 정수를 받아 문자열로 바꾸는 함수입니다. 그런데, 이 함수로 출력하기 전에, 정수값을 변화 시키는 함수(+3)를 먼저 적용 후 문자열로 출력하게 하고 싶습니다. 이럴 경우, 자주 썼던 covariant functor로 시도해 보겠습니다. 위 FPcomplete
의 예시는 (+1) :: Int -> Int
으로 입력과 출력이 Int
로 같아 미스 매치되는게 잘 안보이니, 원래 타입 그대로 두고 매칭 여부를 보겠습니다.
mapMakeString :: (a -> b) -> MakeString a -> MakeString b
MakeString g) = MakeString (g . f)
mapMakeString f (
+3 :: a -> b) (showInt :: a -> String)
mapMakeString (= MakeString ( showInt . (+3) :: b -> String)
이렇게 하면 될 것 같지만, 타입을 적어보면 출력이 맞지 않습니다. (+3) :: a -> b
이니, 그 다음 함수 showInt
는 b -> String
을 받아야 합니다. 하지만 showInt
는 a -> String
입니다. 말로 풀어 보면, (+3)
을 나중에 적용하는 게 아니라, 먼저 적용한 후 showInt
를 적용하고 있습니다. MakeString
타입의 폴리모픽 부분은 출력이 아니라, 입력입니다. mapMakeString
을 적용해서 합성이 이루어질 때, 이 폴리모픽한 부분은 먼저 들어 온 함수의 입력과 만나지 않고, 출력과 만나야만 원하는 동작을 합니다. 소스를 잘 읽어보면 (b -> a)
로 뒤집힌 걸 요구하기 때문에 그냥 covariant functor로 구현할 수 없습니다.
mapMakeString :: (b -> a) -> MakeString a -> MakeString b
MakeString g) = MakeString (g . f) mapMakeString f (
이렇게 Contravariant하게 변하는 걸 fmap하기 위해 아래와 같이 뒤집힌 타입을 메소드로 가진 클래스가 있습니다.
class Contravariant f where
contramap :: (a -> b) -> f b -> f a
결과 타입은 고정되어 있는데, 입력 타입이 달라질 필요가 있을 때 쓸 수 있습니다. (fmap
은 covariant한 경우에 쓴다고 말하는데, 잘 생각해 보면, fmap
은 입력 타입이 고정이고, 결과 타입이 달라질 필요가 있을 때 써왔습니다.)
출력에 있으면 Covariant
입력에 있으면 Contravariant
위와 같은 상황에서도 같은 용어를 쓰는데, 무엇이 기준이고 무엇이 따라 변하는 건지 모호합니다. 이전 섹션의 Contravariant를 보면, a
에서 Some a
로 변환할 때, Some
정의안에 a
의 위치가 SomeType -> a
이면, 특별히 “a와”란 말을 쓰지 않고도, 그냥 Covariant란 말을 쓰고, a -> SomeType
이면 a
와 Contravariant라고 말합니다. functor의 성질이 a
의 위치에 따라 Co, Contra로 바뀝니다.
Produce하면 Covariant
Consume하면 Contravariant
SomeType -> a
이면 결과로 a
를 만들어내니 Produce한다고 말하고, a -> SomeType
은 a
를 인자로 받아 써먹으니 Consume이라 말합니다.
생각 스트레칭
(a -> b) -> b
타입만으로 알 수 있는 게 있습니다.
이 함수는 다른 걸 받는 건 없고,(a -> b)
함수만 받으면b
라는 값을 만들어 냅니다.(a -> b)
함수를 실행하면b
가 나올텐데, 실행하려면a
가 필요합니다. 함수만 받았는데,b
가 나온다는 말은, 안에 이미a
가 있을거라 예상할 수 있습니다. (물론, 받은 함수를 무시할 수도 있겠지만,b
를 generate할 방법은 어떤식으로든 존재해야 합니다.)※ 엄밀히 얘기하면,
forall a => (a -> b) -> b
일 때, 이 함수는a
가 뭔지 모르니,a
를 가지고 있을 방법이 없습니다.(딱 한가지 undefined(bottom)을 가지고 있다고 말할 순 있겠습니다만) 예시는 조금 더 다듬어야 하지만, 이해하는데 도움은 되는 예시니 일단 그대로 두겠습니다. @jason님이 놓친 걸 지적해 주셨습니다. 감사합니다.아래는 사장, 팀장, 요리사, 레시피가 모여 요리를 만드는 코드입니다.
data Meat = Beef | Pork deriving Show data Cooking a = Steak a | Stew a deriving Show newtype Cookbook = Cookbook { runCook :: Meat -> Cooking Meat } steakRecipe :: Meat -> Cooking Meat = Steak steakRecipe chef :: Cookbook -> Cooking Meat = runCook cb Beef chef cb -- leader는 chef가 들어오면 준비된 steak recipe를 넣어 줍니다. leader :: (Cookbook -> Cooking Meat) -> Cooking Meat = ch (Cookbook steakRecipe) leader ch -- boss는 외부에서 leader가 들어오면 Beef를 다루는 chef를 붙여 줍니다. boss :: ((Cookbook -> Cooking Meat) -> Cooking Meat) -> Cooking Meat = ld chef boss ld = print $ boss leader main
위 예시는
boss, leader, chef, recipe
가 모두 있어야 요리가 만들어 집니다.
boss
는leader
없이chef
만 데리고 있습니다.
leader
는recipe
만 가지고 있습니다.
boss
에leader
를 넣어 주면,boss
가 가진chef
를leader
에게 넣어 주고,
leader
는 가지고 있는recipe
를chef
에 넣어 줘서 요리를 만들게 됩니다.
위에서 말한 Consume 모양 중 V -> Int
함수를 받는 고차 함수를 살펴 보겠습니다. reduce하려면 필요한 정보가 어디에 있는지 살펴 보겠습니다.
func1 :: f :: V -> Int
func2 :: ( f :: V -> Int ) -> Int
func3 :: ( g :: ( f :: V -> Int ) -> Int ) -> Int
func4 :: ( h :: ( g :: ( f :: V -> Int ) -> Int ) -> Int ) -> Int
실행되는 코드는 아니고, 고차 함수들이 함수를 인자로 받는 부분에 f
,g
,h
이름을 붙여 놨습니다.
func1
은 외부에서 V
값을 받을테고, - 외부에서 들어온 값을 Consume합니다.func2
는 내부에 f
에 넣을 정해진 V
값이 이미 있고, - 내부에서 Produce합니다.func3
는 f
모양의 “함수” 인자를 가지고 있고, V
값은 g
의 자리로 들어오는 함수가 가지고 있습니다. 즉 외부에서 값이 들어 옵니다. - 외부에서 들어온 값을 Consume합니다.func4
는 g
모양의 “함수” 인자를 가지고 있습니다. 이 함수 안에는 이미 정해진 V
값이 들어 있습니다. - 내부에서 Produce합니다.잘 보면, V
가 contravariant 위치에 있는 함수를 받는 고차 함수를, 차수를 올릴 때마다 Consume, Produce가 반복되고 있습니다.
V
가 Contravariant 위치에 있는 func1
은 Consume이지만,f :: V -> Int
자체가 Contravariant위치에 있으면 Produce하게 됩니다.g :: ( V -> Int ) -> Int
자체를 Contravariant에 두면, 다시 Consume으로 바뀝니다.h :: (( V -> Int ) -> Int ) -> Int)
자체를 Contravariant 두면, 또 다시 Produce로 바뀝니다.마치 마이너스(-), 즉 Negative 기호 연산(음수 x 음수 = 양수)을 반복하는 것 같은 모양이 나옵니다. Contravariant인 것을 Contravariant에 두면 Covariant로 바뀌고 있습니다. 이런 성질 때문에 Negative, Positive로 표현하기도 합니다.
@재원님이
(a -> b) -> b
형태의 함수는 안에a
를 가지고 있다는 설명 방식에 헛점이 있다고 지적해 주셨습니다. 왜냐하면 꼭a
를 가지고 있지 않고, 함수 자체를 써먹지 않고 버리고, 어떤 방법으로a
를 만들어 낼 수도 있으니 “반드시”a
를 가지고 있을 거라 설명하면 안된다는 말씀입니다. 물론, 맞는 말씀인데, 여기선 직관을 위해 “대부분” 그럴거란 가정하에 그대로 두었습니다.a
를 미리 가지고 있다 =a
를 생성generate 한다와 같은 의미로 봐주시면 되겠습니다.
@다믜님의 Positive/Negative 설명입니다. @다믜님 허락하에 원문을 올립니다.
a -> b
를 구현할 때에는a
가 주어지니 이걸 사용하기만 하면 되죠. negative입니다.
(a -> b) -> c
의 경우a -> b
가 주어지는데, 이걸 사용하는 유일한 방법은a -> b
에 인자로a
를 주는것입니다. 결국a
를 만들어줘야 하죠. positive입니다.
((a -> b) -> c) -> d
에서는(a -> b) -> c
가 주어지고 이걸 사용하려면a -> b
를 건네줘야 하죠. 그리고a -> b
를 만들 때에는 주어진a
를 사용하면 되니 결국a
를 받는 꼴이 됩니다. negative가 되지요negative/positive를 이해하는 다른 좋은 방법으로 monotonic function이 있는데요
increasing function은 postivive, decreasing function은 negative에 대응이 됩니다. increasing function은a ≤ b → f(a) ≤ f(b)
를 만족하는 함수f
, decreasing function은b ≤ a → f(a) ≤ f(b)
를 만족하는 함수f
를 말하는데, 이것만 보더라도 이미 co/contra-variant functor와의 유사성이 보이죠
그리고 increasing/decreasing function을 합성할 때 decreasing function이 짝수 번 나오면 increasing, 홀수 번 나오면 decreasing이 된다는걸 쉽게 할 수 있습니다 가령
f
,g
가 decreasing function이라고 할 때f∘g
가 어떻게 되는지 보면
1.a ≤ b
이면g(b) ≤ g(a)
(g
가 decreasing 이므로)
2.g(b) ≤ g(a)
이면f(g(a)) ≤ f(g(b))
(f
가 decreasing 이므로)
3. 1,2 를 합하면a ≤ b
일 때f(g(a)) ≤ f(g(b))
. 즉f∘g
는 increasing. 이렇게 됩니다.
이걸 굳이 monotonic function으로 설명한 이유는 이제 그래프로 볼 수가 있어서 그렇습니다.각각 increasing/decreasing function의 그래프구요 decreasing function 두개를 합성하면 increasing이 된다는건 아무 점이나 두개찍고 대소관계를 비교해보면 쉽게 느낄 수 있습니다 negative/positive란 용어는 논리학에서 왔구요, 함수를 대상으로 하는 경우에는 monotonic, increasing/decreasing이라는 표현을 주로 사용하죠. 카테고리에서 이제 covariant/contravariant라는 표현을 쓰구요
※ 참고
정적 타입 지정과 변성 - 오현석
FPcomplete Covariance and Contravariance