Int 와 Maybe Int의 차이. (값을 감싸는wrapping 이유)

Posted on June 24, 2020

추상화

A와 전혀 다른 B가 있는데, 둘을 때에 따라 같은 걸로 볼 필요가 있다고 칩시다. 그럴 땐 data C = A | B 란 타입을 만들어, 둘 모두 C라고 부를 수 있습니다. A를 다루던 함수와 B를 다루던 함수는 그대로 적용할 순 없는, 다른 타입이 되었습니다. A를 다루는 함수를 C에 넣어주면, 안에 들어 있는 값이 A일 때는 적용하고, 아니면 그냥 아무 일도 일어나지 않게 하면 됩니다. B를 다루던 함수도 마찬가지입니다. 안에 들어있는 게 B인 경우만 적용하고, 아니면 그대로 두면 됩니다.

패턴 매칭으로 갈래길을 만든다.

1,2,3 ... 이 노는 동네와, Just 1, Just 2, Just 3 ... 동네는 다르다?
1에서 Just 1로 가려면 Just 생성자 함수를 쓰면 됩니다. 생성자 함수를 써서 ‘무언가’ 정보 하나를 추가해 격리된 동네로 보냅니다. 단순 꼬리표처럼 보이는 Just가 붙어 있는 값들은 Maybe 동네에서만 있을 때는 ‘무언가’ 정보가 드러나지 않지만, 격리된 동네를 벗어나서 다른 값들과 어울리려면 그 때 ‘무언가’ 정보가 드러나게 됩니다. 갈 때 생성자 함수를 썼듯이, 올 때도 그냥 쓰면 될 것 같지만, 모든 래핑 타입은 ‘무언가’ 정보를 추가하기 때문에 거꾸로inverse로 오려면 정보를 처리하든지, 버리면서 와야 합니다. 그냥은 올 수 없습니다.

예를 들어, Maybe에서 ’무언가’를 꺼내는 방법(Maybe a -> a)은, 반드시

Just값, Nothing 갈래 길 중 어디로 갈지 선택(분기)하는 작업”

instance Functor Maybe where  
    fmap f (Just x) = Just (f x)  
    fmap f Nothing = Nothing 

을 통해서만 꺼낼 수 있습니다. 다른 방법은 없습니다. Maybe 값을 꺼내 오려면(다른 말로 Maybe 특징이 발현되려면) 코드 어딘가에는 반드시 분기문이 존재합니다. 어떤 경우에는 직접 길을 고르고, 어떤 경우에는 미리 만들어 놓은 함수(fmap)를 쓰기도 합니다. if를 쓰든, case를 쓰든 분기 작업을 해야합니다. 하스켈에서 가장 많이 쓰이는 분기문은 if도 아니고, case도 아니고, 바로 패턴 매칭입니다. fmap 함수도 패턴 매칭으로 갈 길을 선택합니다. 함수 정의부에서 패턴 매칭 할 수도 있고, case 구문에서 할 수도 있고, let 구문이나 <- 구문으로 패턴 매칭할 수도 있습니다. 어디가 됐든, 반드시 “갈래 길 중 선택”하는 구문은 반드시 나타납니다.

하스켈로 연습 문제를 푸는 수준을 넘어가면 수 많은 래핑 타입들로 로직을 설계합니다. 한 번만 싸인게 아니라, 생성자들로 여러번 싸여 있는 값들을 만나게 됩니다. 그런 값들은 생성자를 풀 때마다 특정 작업을 하도록 되어 있다고 보면 됩니다.

모나드에서 m a 값과 a -> m b 함수를 붙이는 이유입니다. m am a -> m b를 붙이면 편할텐데, 그럼 m의 effect가 발현된다는 보장이 없습니다. m aa -> m b를 적용하려면 반드시 m을 벗겨내는 작업을 해야 합니다. 비수학적으로 모나드를 이해할 때 필요한 개념입니다.

값이 아니라 “값을 줄 준비”

Computation = 준비
Maybe Int는 평범한 정수가 아닙니다. 어떤 절차를 거치면 정수를 줄 준비라고 볼 수 있습니다. Maybe를 벗겨내는 절차 없인 그저 준비만 하고 있는 상태기 때문에 다른 정수들과 어울릴(연산할) 수 없습니다. Int는 정수고, Maybe Int어떤 절차를 거친 후 정수를 줄 준비입니다.

Reader env Intenv를 받아서 어떤 처리를 한 다음 Int를 돌려줄 준비를 한 상태입니다. 생성자나 함수를 준비로 해석하는게 언제나 들어 맞는지는 아직 확신이 서진 않지만, 래핑에 래핑에 래핑을 만나 복잡해 보일 때 준비로 해석하면 도움이 될 때가 있습니다.

m a는 a와 비슷하다.

m aa와 아예 다른 값이 아닙니다. m aa속성은 그대로 가지고 있는 때에 따라 그대로 가지고 있을 수도 있는 닮은 타입입니다. 닮은 타입간의 관계를 다룰 때 모나드가 등장합니다.

타입 클래스

타입을 래핑하는 또 다른 목적은, 같이 쓰일 함수를 고르는 작업을 위해서입니다.(보통 newtype을 이용합니다.) 타입 클래스와 인스턴스를 쓰면, 타입에 따른 함수를 고를 수 있습니다. 1

이 포스트의 결론은, 래핑 a

  1. 갈래 길(코드의 분기)을 내포하고 있거나,(추상화와 같은 말)
  2. 준비 상태를 뜻하거나,
  3. 특정 함수셋을 고르기 위한 태그입니다.

2020.10.6 추가

감싼 타입에 함수 적용

타입을 감싸 또 다른 타입을 만드는 작업이 필요한 이유를 좀 더 선명하게 알아야 합니다. 유독 함수형에서만 일어나는 일은 아닙니다. 프로그래밍을 하다 보면 같은 속성을 가지지만 구조는 다른 경우, 역으로 다른 속성을 가지지만 구조는 같은 경우들을 만납니다. 퍼포먼스 때문일 때도 있고, 안전성을 위해서일 때도 있고, 논리를 따라가는 인간의 기억력을 보조해주기 위해서일 때도 있습니다. 감싸면서 정보를 추가하거나, 다른 것과 구별되도록 합니다. 이렇게 감싼 타입에 함수를 적용하는 방식은 대부분

  1. 감싼 걸 풀어내고
  2. 풀어 낼 때 항상해야 되는 작업을 하고,
  3. 안에 들어 있는 값에 함수를 적용
  4. 그런 후에 다시 감싼 타입으로 만듭니다.

이런 작업은 모든 감싼 타입에서 일어납니다. 모든 감싼 타입 마다 일일이 푸는 방법을 기억해야 합니다. 여기까지 생각이 미치면, 추상화 작업을 해서 뇌의 부하를 줄여줘야겠다는 생각이 들게 됩니다. 푸는 작업을 fmap 한가지로 추상화하자고 약속을 정한게 바로 펑크터입니다.

하스켈에서 함수 하나 하나는 모두 독립적이며, 보통 Lazy하기 때문에 언제 실행될지도 모릅니다. 이런 상태에서 프로그램의 흐름2을 강제로 만들어낼 필요가 있을 때는 함수가 함수를 감싸는composition 방법을 씁니다. (보통 composition을 합성이라 번역하는데, 딱 적합하다는 생각이 들지 않습니다. f . g 의 결과는 g를 적용한 후 f를 적용한다는 개념인데, 합성과는 좀 달리 느껴집니다. 조합이란 말이 더 어울리는 것 같기도 합니다.) 웬만하면 composition이 가능해야 여기 저기 쓰임새가 생깁니다. a -> b , b -> c 함수를 compostion 하려면 그냥 (.)을 쓰면 되고, m a -> m b, m b -> m c(.)을 쓰면 됩니다. 그럼 a -> m b, b -> m c 함수는 어떻게 composition하면 될까요? 여기도 모나드를 이해하기 위한 시작점 중에 하나입니다.
모나드,같음 글을 참고하세요.

2021.5.9 추가

값 생성자를 벗길 때 발현되는 컨텍스트

얼굴을 봐야 대화를 하지

a -> f b 해석

a를 받아 작업을 한 후 f b의 결과를 만드는데, b가 아니라 f b인건 반드시 어떤 값 생성자로 감싸져 있다는 뜻입니다.(카인드로 얘기하면 * -> *) 값 생성자로 감싸져 있으면 무슨일이 생길까요? 값 생성자로 감싸는 이유는 값 생성자를 벗겨낼 때 어떤 작업을 반드시 하게끔 하기 위해서 입니다. 벗겨낼(inverse) 때 반드시 하는 작업을 effect라 부릅니다. 유심히 볼 동작은 값 생성자로 감쌀때가 아닙니다. 값 생성자와 패턴 매칭으로 풀어낼 때 비로소 effect가 발현됩니다. 그리고 또 한가지, 값 생성자를 벗겨 낼 때는 옆에 있는 함수와 뭔가를 하기 위해서 벗겨냅니다. 혼자만 effect를 확인하는 건 의미가 없습니다. effect를 확인 후 다른 함수와의 동작을 결정지을 필요가 있을 때 값 생성자를 벗겨 냅니다. effect가 있는 작업들을 연이어 합성했을 때, 함수가 연결될 때마다 고정적으로 작동하는 effect작업을 context라 부릅니다.

* -> * 카인드를 만났을 때 해석하는 용어를 알았으니 적용해서 읽어 보겠습니다.

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

f컨텍스트로 만드는 함수를 받아, t컨텍스트 안에 들어 있는 a에 적용해서 f컨텍스트 ( t컨텍스트 a)를 만든다.
ft를 구체 타입으로 바꿔서 읽어 보겠습니다. Maybe를 만드는 함수를 받아, [a]에 적용해서 Maybe [a]를 만듭니다. 보통 하스켈 문서들이 여기서 설명을 끝내서 난감하게 만듭니다. 왜 이런 작업을 할까요? 실제 코딩하다 보면 만나는 상황을 상상해봐야 합니다.

func :: a -> Maybe a
func | x > 0 = Just x
     | _     = Nothing

이 함수를 적용해야 하는 대상들이 하나가 아니라서 리스트에 담아두었다면, 어떻게 적용할까요? fmap, traverse 둘 모두 가능합니다.
전체가 아닌 각각이 별도의 의미를 가진다면 fmap으로 각각의 값들을 Maybe값들로 바꾸어 놓고 다시 리스트로 싸놓으면 되고,
모두 양수일 때만 의미가 있다면, 바로 이 때 traverse를 등장시키면 됩니다. 함수를 적용하되, Maybe 컨텍스트를 발현시키겠다는 말입니다.

리스트의 fs <*> xs applicative 정의는 앞에 것도 컨텍스트를 벗겨내고, 뒤에 것도 컨텍스트를 벗겨내서 f x 값을 모으겠다는 말입니다.

컨텍스트 발현
컨텍스트 실행
컨텍스트를 벗겨내고

이게 다 같은 말로 쓰입니다.

함수 같은 값 생성자

Maybe IntJust Int값 아니면 Nothing값이 될 수 있습니다. Int값을 Just Int로 만들려면, 그냥 Just로 감싸면 됩니다. 마치 Just가 인자로 Int를 받아 Just Int가 되는 함수 같습니다. 하지만, 일반 함수와 다르게 값 생성자는 패턴 매칭에 쓰일 수 있습니다. 이게 헛갈릴까 싶지만 처음 공부할때 혼동하는 경우가 있습니다.

Maybe Int는 당분간은 Maybe

필요로 하는 값의 타입은 Int이고, Maybe는 어떤 절차를 나타내는 구조라 부르기도 합니다. [Int]의 구조는 리스트, Reader env Int의 구조는 Reader env라고 보면 됩니다. 항상 구조를 파고 들어 값을 가져오는 건 아닙니다. 구조가 뭔지만 궁금할 때도 있습니다. 하스켈은 Lazy하니 Maybe IntInt를 필요로 하는 순간이 오기 전까진, 그냥 Maybe 구조의 뭔가입니다. 보통 패턴 매칭으로 구조를 벗겨 냅니다. Maybe IntJustNothing으로 패턴 매칭하는 순간 Int인 걸 알게 됩니다.


  1. 타입에 따라 인스턴스를 고르니, 타입과 함수를 연관 짓는 함수로 볼 수 있습니다. 어떤 것과 어떤 것을 연관 지어 놓는 걸 함수라고 하는데, 어떤 것을 연관 짓냐에 따라 이름을 달리 붙여 놨습니다.

    1. 타입과 타입을 매핑 = 함수
    2. 타입과 함수를 매핑 = 타입 클래스
    3. 함수와 함수를 매핑 = 펑크터
    4. 펑크터와 펑크터를 매핑 = Natural Transform

    펑크터는 함수만 매핑하는 것이 아니라, 타입도 같이 매핑합니다.
    a, b타입이 있고, 이 타입들을 다루는 a -> b 함수가 있다면, 펑크터 FF a, F b, F a -> F b로 매핑합니다.↩︎

  2. main 함수에서 흐름이 시작되는데, main 함수 자체가 >>= 로 엮어 놓은 거대한 함수 composition입니다.↩︎

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