함수가 들어 있는 데이터 타입의 펑크터 만들기

Posted on May 7, 2021

여기 포스트에선 값이 아닌, 함수를 구조안에 가지고 있는 데이터 타입을 위한 펑크터를 정의하는 연습을 하는 게 목적입니다. 함수 안에 들어있는 값에 대한 접근은 역시 매개 변수 말고는 없습니다. 고차 함수 스타일의 구조, 즉 안에 먼저 값을 가지고 있고, 함수를 기다리는 타입은 넣어 줄 함수를 필요한 형태로 변형해서 넣어줘야 합니다.

아래 사이트를 보기 전에 준비 운동으로 볼만한 포스트입니다. 여기서 예시로 보인 소스들도 아래 사이트에 있는 소스를 이해를 돕기위해 네이밍을 다시한 정도의 소스입니다.

Covariance and Contravariance - FPComplete

속성을 가지고 있는 고차 함수

주인공 함수 등장

withRandom 함수는 내부에 랜덤값을 가지고 있다가, 인자로 함수가 들어오면 랜덤값에 함수를 적용하는 함수입니다. 흔히 보이는 값을 먼저 쥐고 있고, 함수를 받으면 적용할 준비를 하는 패턴입니다.

import System.Random

withRandom :: (Int -> IO ()) -> IO ()
withRandom = \callback -> do
    int <- randomRIO (1, 10)
    callback int

main :: IO ()
main = do
    withRandom $ print 

랜덤값에 1000을 항상 더해서 보여주려면

withRandom $ print . (+1000)

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
withRandom = WithValue $ \callback -> do
    int <- randomRIO (1, 10)
    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 ()
main = do
    flip runWithValue print $ fmap (+1000) $ fmap (+2000) withRandom

runWithValue ( ... ) print 이렇게 두 번째 인자 때문에 괄호를 쓰는 걸 피하기 위해 흔히 flip을 써서 인자 순서를 뒤집습니다.

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