Effect의 결합 법칙

Posted on December 7, 2022

어떤 구조가 교환 법칙을 만족한다든지, 결합 법칙을 만족한다든지 하면 어떤 효과가 있을까요?

프로그래밍에선 결합 법칙이 있고, 항등원이 있는 모노이드가 매우 중요한 대접을 받고 있습니다. 왜 그럴까요?

실행 시점에 자유롭지만 순서는 지켜야 하는 구조가 주는 이점을 고민해 봤습니다. (역시나, 관련 텍스트나 언급하는 자료들을 거의 볼 수 없어 상상 반입니다.)

결합 법칙을 따르지 않으면?

f1 = \x -> x * 2
f2 = \x -> x + 1
f3 = \x -> x +10

odot f1 f2 = \x -> f2 (f1 x)
ndot f1 f2 = \x -> f2 (f1 $ x + 2)

main = do
  print $ (f1 `odot` f2) `odot` f3  $ 1
  print $  f1 `odot` (f2 `odot` f3) $ 1
  print $ (f1 `ndot` f2) `ndot` f3  $ 1
  print $  f1 `ndot` (f2 `ndot` f3) $ 1

ghci> main
13
13
21
19

odot은 결합 법칙이 성립하지만, ndot은 그렇지 않습니다.
※ odot은 original dot (.)을 의미하고, ndot은 new dot이란 의미입니다.

effect가 만들어지는 순간에는 이전 effect에 의존하지 않아야 한다.

bind의 경우 결합 법칙을 만족하려면, 새로 effect가 만들어 질 때는 어디에도 의존하지 않고 effect가 만들어져야 합니다. bind 타입을 봐도 알 수 있습니다. 두 번 째 인자로 effect를 새로 만드는 a -> m b 함수를 받는데, m은 이 전 effect에 의존하지 않고 만들어집니다. 만일, 이전 effect에 의존한다면 m a -> m b 함수 모양이었을 겁니다.

값을 받기 전 함수들을 먼저 Compose한 걸 함수에 넘겨 주거나 받기 어렵습니다. 즉 고차 함수에 쓰기 어렵습니다.

아직 들어오지 않은(알지 못한 값에 의존하는) 경우와 그렇지 않은 경우의 차이를 보겠습니다. 말만 들어서는 금방 이해가지 않을 수 있습니다. 아래 예를 보겠습니다.

odot은 결합 법칙이 성립하지만, ndot은 그렇지 않습니다.

\x -> f3 (  (\y -> f2 (f1 $ y + 2))  $ x + 2)
                       *2
                   +1
      +10

1을 넣으면 결과는 21

\x ->  (\y -> f3 (f2 $ y + 2)) (f1 $ x + 2)
                                *2
                  +1
              +10

1을 넣으면 결과는 19

f1, f2, f3 실행 순서는 둘 다 같지만, ndot+2를 몇 번 하고 시작하는지가 다릅니다.

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