Apply - Price와 (exRate -> Price)를 다루는 프로그램의 골격을 똑같이 하고 싶어

Posted on June 19, 2025

펑터와 Applicatives가 왜 필요했는지 비수학적으로 풀어 봤습니다. 아래가 가장 좋은 이해 방법이라 주장하는 건 아닙니다. 이렇게도 생각해 볼 수 있다 정도로 받아들이시면 좋겠습니다.

가장 기본적인 적용

환율 적용 가격표

면세점에서 옷을 팔면서, 환율을 적용한 가격표와 그렇지 않은 가격표를 붙이는 걸 상상해 보겠습니다. 먼저 보통의 고정된 가격표입니다.

type Price = Double

50프로 할인 금액을 붙이고 싶습니다.

half = \price -> price * (1/2)

특별할 것 없이 함수를 가격에 적용하면 됩니다.

half $ price

$가 안보여도 되지만, 나중에 설명이 이어지도록 apply가 명확히 보이게 했습니다.

티셔츠와 바지를 산 경우 이를 합산하려면, 그냥 더하면 됩니다.

sum = t-price + p-price

펑터의 적용

이제 가격표를 붙이는데, 고정된 금액 Price가 아니라, 시시각각 변하는 환율exRate을 넣으면 값이 되는 ExRate -> Price로 붙이는 걸 가정하겠습니다. 똑똑한 전자 가격표쯤으로 보겠습니다.

type ExRate = Double
newtype ExRatePrice a = ExRatePrice (ExRate -> a)

위에서 정의했던 half 함수를, 가격이 변동되게 적용하려면 $만으론 적용할 수 없게 됐습니다. 먼저 환율 exRateexRate -> Price 함수에 넣고 나온 결과 Pricehalf를 적용해야 합니다.

f <$> (ExRatePrice ep) = ExRatePrice (\exRate -> f . ep $ exRate)

“환율을 적용하면 값이 되는” ExRatePrice타입이 가진 함수 ep를 환율에 적용하고, 결과에 half같은 함수를 적용하는 걸, 함수 합성1 f . ep로 표현하고 있습니다. 이제 50프로 할인 가격은 다음처럼 붙이면 됩니다.

half <$> exRatePrice

halfExRatePrice에 적용하기 위해, half를 재정의하지 않고, half적용하는 $를 재정의 했습니다.

Applicatives의 적용

다음으로, 티셔츠와 바지를 합산하려면?
Double 두 개를 받아 더하는 + 함수를 두 개의 ExRatePrice에 적용해야 합니다.

tep = \exRate -> exRate * t-price
pep = \exRate -> exRate * p-price

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에 적용하는 미션이 됐습니다.

컨텍스트가 없는 함수를 컨텍스트가 있는 값에 적용할 때 <$>를 썼는데,
컨텍스트가 있는 함수컨텍스트가 있는 값에 적용하는 방법이 필요해졌습니다.

f <$> ExRatePrice tep <*> ExRatePrice pep = \exRate -> (\b -> f (tep exRate) b) . pep exRate

위와 같이 <*>를 정의하면,

(+) <$> ExRatePrice tep <*> ExRatePrice pep

로 간편한 모양으로 쓸 수 있게 되었습니다.

달리 말하면, (+)ExRatePrice에 적용하기 위해 (+)를 재정의하지 않고, (+)적용하는 $를 재정의 했습니다.

적용 APPLY

각 상황에서의 “적용” 모습을 모아서 보겠습니다.

half  $  price                               -- 함수를 값에 적용

half <$> ExRatePrice ep                      -- 함수를 컨텍스트를 가진 값에 적용

(+)  <$> ExRatePrice tep <*> ExRatePrice pep -- 컨텍스트를 가진 함수를, 컨텍스트를 가진 값에 적용

각 상황에 맞는 연산자를 정의하니, “적용” 모양이 닮아 보이지 않나요?

목표는 Price의 구조가 ExRatePrice 구조에도 그대로 살아 있기 때문에 (닮았기 때문에),
Price를 다루는 코드 모양과, ExRatePrice를 다루는 코드 모양을 비슷하게(혹은 같게) 만드는 것입니다

이 글을 읽고나서,

로 읽을 수 있길 바랍니다.

λ> 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 함수를 합성하려면? 으로 넘어갑니다. 다들 아시는 그 거지요.


  1. 펑터, Applicatives를 구현하는데 (t -> Double)에 있는 Double꺼내와 (Double -> Double)를 적용하는 것으로 읽으면 안됩니다. 함수는 원자적인 것으로, 이를 해체해서 안을 들여다 볼 수 없습니다. 그저 입력을 주면 출력을 해준다는 것만 알 뿐입니다. 다시 말해 함수 안에서 벌어지는 일에 개입할 수 없다는 얘기입니다. 예를 들어, 어차피 나중에 -1을 할 것이니, 함수 안에서 +1을 하는 작업을 안하고, 바깥에서 -1도 안하고 싶어도, 그럴 수 없습니다. +1 하도록 만든 함수는 +1할 뿐입니다. 그래서, 함수를 가지고 있는 타입들에선, 펑터나 Applicatives 구현들이, 내부 함수들과 합성을 하는 모양이 나옵니다.↩︎

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