펑터와 Applicatives가 왜 필요했는지 비수학적으로 풀어 봤습니다. 아래가 가장 좋은 이해 방법이라 주장하는 건 아닙니다. 이렇게도 생각해 볼 수 있다 정도로 받아들이시면 좋겠습니다.
면세점에서 옷을 팔면서, 환율을 적용한 가격표와 그렇지 않은 가격표를 붙이는 걸 상상해 보겠습니다. 먼저 보통의 고정된 가격표입니다.
type Price = Double
50프로 할인 금액을 붙이고 싶습니다.
= \price -> price * (1/2) half
특별할 것 없이 함수를 가격에 적용하면 됩니다.
$ price half
※ $
가 안보여도 되지만, 나중에 설명이 이어지도록 apply가 명확히 보이게 했습니다.
티셔츠와 바지를 산 경우 이를 합산하려면, 그냥 더하면 됩니다.
sum = t-price + p-price
이제 가격표를 붙이는데, 고정된 금액 Price
가 아니라, 시시각각 변하는 환율exRate
을 넣으면 값이 되는 ExRate -> Price
로 붙이는 걸 가정하겠습니다. 똑똑한 전자 가격표쯤으로 보겠습니다.
type ExRate = Double
newtype ExRatePrice a = ExRatePrice (ExRate -> a)
위에서 정의했던 half
함수를, 가격이 변동되게 적용하려면 $
만으론 적용할 수 없게 됐습니다.
먼저 환율 exRate
를 exRate -> Price
함수에 넣고 나온 결과 Price
에 half
를 적용해야 합니다.
<$> (ExRatePrice ep) = ExRatePrice (\exRate -> f . ep $ exRate) f
“환율을 적용하면 값이 되는” ExRatePrice
타입이 가진 함수 ep
를 환율에 적용하고, 결과에 half
같은 함수를 적용하는 걸, 함수 합성1 f . ep
로 표현하고 있습니다. 이제 50프로 할인 가격은 다음처럼 붙이면 됩니다.
<$> exRatePrice half
half
를 ExRatePrice
에 적용하기 위해, half
를 재정의하지 않고, half
를 적용하는 $
를 재정의 했습니다.
다음으로, 티셔츠와 바지를 합산하려면?
Double
두 개를 받아 더하는 +
함수를 두 개의 ExRatePrice
에 적용해야 합니다.
= \exRate -> exRate * t-price
tep = \exRate -> exRate * p-price
pep
ExRatePrice tep + ExRatePrice pep
(+)
는 Price
(Double
타입)에 적용하는 함수라, 이렇게는 사용할 수 없습니다.
(+)
를 \a -> (\b -> a + b)
로 커링해서, 위에서 정의한 <$>
를 적용해 보겠습니다.
+) <$> (ExRatePrice tep)
(= ExRatePrice (\exRate -> (\a -> (\b -> a + b)) . tep $ exRate)
이제 티셔츠 가격이 먼저 적용된 +
를 가진, ExRatePrice (\exRate -> (\b -> (t-price * exRate) + b))
를 ExRatePrice pep
에 적용하는 미션이 됐습니다.
컨텍스트가 없는 함수를 컨텍스트가 있는 값에 적용할 때 <$>
를 썼는데,
컨텍스트가 있는 함수를 컨텍스트가 있는 값에 적용하는 방법이 필요해졌습니다.
<$> ExRatePrice tep <*> ExRatePrice pep = \exRate -> (\b -> f (tep exRate) b) . pep exRate f
위와 같이 <*>
를 정의하면,
+) <$> ExRatePrice tep <*> ExRatePrice pep (
로 간편한 모양으로 쓸 수 있게 되었습니다.
달리 말하면, (+)
를 ExRatePrice
에 적용하기 위해 (+)
를 재정의하지 않고, (+)
를 적용하는 $
를 재정의 했습니다.
각 상황에서의 “적용” 모습을 모아서 보겠습니다.
$ price -- 함수를 값에 적용
half
<$> ExRatePrice ep -- 함수를 컨텍스트를 가진 값에 적용
half
+) <$> ExRatePrice tep <*> ExRatePrice pep -- 컨텍스트를 가진 함수를, 컨텍스트를 가진 값에 적용 (
각 상황에 맞는 연산자를 정의하니, “적용” 모양이 닮아 보이지 않나요?
목표는 Price의 구조가 ExRatePrice 구조에도 그대로 살아 있기 때문에 (닮았기 때문에),
Price를 다루는 코드 모양과, ExRatePrice를 다루는 코드 모양을 비슷하게(혹은 같게) 만드는 것입니다
이 글을 읽고나서,
(+) <$> ExRatePrice tep <*> ExRatePrice pep
같은 것들을 보면, 컨텍스트를 처리하며(컨텍스트가 있는 상태에서) (+)
를 하고 있구나,로 읽을 수 있길 바랍니다.
λ> type ExRate = Double
λ> newtype ExRatePrice a = ExRatePrice (ExRate -> a)
λ> f <$> (ExRatePrice ep) = ExRatePrice (\exRate -> f . ep $ exRate)
λ> ExRatePrice f <*> ExRatePrice ep = \exRate -> (f exRate) (ep exRate)
λ> tep = \e -> e * 100
λ> pep = \e -> e * 1000
λ> (+) <$> ExRatePrice tep <*> ExRatePrice pep $ 1.0 1100.0
Price -> Price
함수를 (.)
으로 합성했는데, Price -> ExRatePrice
함수를 합성하려면? 으로 넘어갑니다. 다들 아시는 그 거지요.
펑터, Applicatives를 구현하는데 (t -> Double)
에 있는 Double
을 꺼내와 (Double -> Double)
를 적용하는 것으로 읽으면 안됩니다. 함수는 원자적인 것으로, 이를 해체해서 안을 들여다 볼 수 없습니다. 그저 입력을 주면 출력을 해준다는 것만 알 뿐입니다. 다시 말해 함수 안에서 벌어지는 일에 개입할 수 없다는 얘기입니다. 예를 들어, 어차피 나중에 -1
을 할 것이니, 함수 안에서 +1
을 하는 작업을 안하고, 바깥에서 -1
도 안하고 싶어도, 그럴 수 없습니다. +1
하도록 만든 함수는 +1
할 뿐입니다. 그래서, 함수를 가지고 있는 타입들에선, 펑터나 Applicatives 구현들이, 내부 함수들과 합성을 하는 모양이 나옵니다.↩︎