여기 포스트에선 값이 아닌, 함수를 구조안에 가지고 있는 데이터 타입을 위한 펑크터를 정의하는 연습을 하는 게 목적입니다. 함수 안에 들어있는 값에 대한 접근은 역시 매개 변수 말고는 없습니다. 고차 함수 스타일의 구조, 즉 안에 먼저 값을 가지고 있고, 함수를 기다리는 타입은 넣어 줄 함수를 필요한 형태로 변형해서 넣어줘야 합니다.
아래 사이트를 보기 전에 준비 운동으로 볼만한 포스트입니다. 여기서 예시로 보인 소스들도 아래 사이트에 있는 소스를 이해를 돕기위해 네이밍을 다시한 정도의 소스입니다.
Covariance and Contravariance - FPComplete
withRandom
함수는 내부에 랜덤값을 가지고 있다가, 인자로 함수가 들어오면 랜덤값에 함수를 적용하는 함수입니다. 흔히 보이는 값을 먼저 쥐고 있고, 함수를 받으면 적용할 준비를 하는 패턴입니다.
import System.Random
withRandom :: (Int -> IO ()) -> IO ()
= \callback -> do
withRandom <- randomRIO (1, 10)
int
callback int
main :: IO ()
= do
main $ print withRandom
랜덤값에 1000을 항상 더해서 보여주려면
$ print . (+1000) withRandom
withRandom
을 부를 때, 적용할 함수를 미리 다 묶어서 준비해야 하니, 유연성이 떨어집니다. 컴포지션 스타일로 계속 함수를 적용할 수 있게 하려고 합니다. 구조안에 들어있는 값에 함수를 적용할 때 적합한 패턴이 Functor
로 정의하고 fmap
을 쓰는 겁니다.
fmap f3 $ fmap f2 $ fmap f1 withRandom ) print (
이런 모양쯤을 미리 예상해 볼 수 있습니다. withRandom
은 함수고, 그 안에 랜덤값이 들어 있는데, 랜덤값에 함수를 적용하려면 fmap
을 어떻게 정의해야 할까요? 코모나드 포스트에서 보았듯이 이럴 경우 함수 인자로 값을 뽑아내는 함수를 넣거나, 변형하는 함수를 직접 넣어주는 작업을 하면 됩니다. (지극히 프로그래밍적인 사고인데, 이 걸 “고급스럽게” 수학적으로 보는 훈련이 필요할 것 같긴 한데 쉽지 않네요.)
withRandom
은 결과 타입이 IO
이기 때문에, 값을 꺼내올 수 없어 컴포지션할 함수를 callback
에 묶어서 같이 넣어 안에서 변형해야 합니다. 일단 Functor
인스턴스를 만들려면 fmap
을 고를 키로 쓰일 타입을 정의해야 합니다.
newtype WithValue a = WithValue { runWithValue :: (a -> IO ()) -> IO () }
withRandom :: WithValue Int
...
이렇게 준비한 후 WithValue
타입을 위한 Functor
인스턴스를 정의하면 됩니다.
instance Functor WithValue where
fmap f wv = ...
아래처럼 쓰기 위한 fmap
을 정의해야 합니다.
> runWithValue (fmap (+1) $ fmap (+2) $ withRandom) print
callback
이 들어가기 전에 f와 묶여서 들어가야 합니다.
fmap f (WithValue wvAction) = WithValue (\callback -> wvAction (callback . f))
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
-- 패턴 매칭으로 action을 꺼내고 액션에 callback을 넣기 전에 f와 묶어서 넣습니다.
전체 코드는 다음과 같습니다.
withRandom :: WithValue Int
= WithValue $ \callback -> do
withRandom <- randomRIO (1, 10)
int
callback int
newtype WithValue a = WithValue { runWithValue :: (a -> IO ()) -> IO () }
instance Functor WithValue where
fmap f (WithValue wvAction) = WithValue (\callback -> wvAction (callback . f))
main :: IO ()
= do
main flip runWithValue print $ fmap (+1000) $ fmap (+2000) withRandom
※ runWithValue ( ... ) print
이렇게 두 번째 인자 때문에 괄호를 쓰는 걸 피하기 위해 흔히 flip
을 써서 인자 순서를 뒤집습니다.