haskell study 13
TRANSCRIPT
function type
함수의 타입에 대해 한 번 이야기해봅시다. a 타입의 값을 받아 b 타입의 값을 반환하는 함수가
있다면 이 함수의 타입은 뭐가 될까요? 당연히 a -> b 타입이 되겠죠. 여기서 ->에 주목해봅시다.
이 타입을 일종의 타입을 만들어내는 타입 생성자라고 생각해봐요. kind가 * -> * -> *인 타입
생성자인거죠. 저걸 전위 표기법으로 바꾸면 아래와 같이 될 겁니다.
(->) a b
그럼 여기서 부분 적용된 (->) a는 어떤 의미를 갖게 될 까요? 이걸 다시 중위 표기로 나타내면
(a->) 가 될겁니다. 인자의 타입이 a이고, 리턴 타입은 뭐가 될지 알 수 없는 함수인 거죠.
중요한 건 이 개념을 어디에 적용할 수 있느냐겠죠. 놀랍게도, (->) a를 타입 인자를 하나 받아서
새로운 타입(함수겠죠)을 리턴하는 타입 생성자로 보면, (->) a 타입 생성자에 대한 Functor,
Applicative Functor, Monad 인스턴스를 모두 만들 수 있습니다. 하나씩 살펴보죠.
function Functor
instance Functor ((->) r) where
fmap f g = (\x -> f (g x))
함수 타입에 대한 Functor는 위와 같이 정의되어 있습니다. 대충 봐서는 이해하기 어렵죠. fmap의
타입 서명부터 차근 차근 살펴봅시다.
fmap :: (a -> b) -> f a -> f b
지금은 (->) r에 대한 Functor 인스턴스를 구현하고 있으니 여기서 f는 (->) r이 되겠죠
fmap :: (a -> b) -> (->) r a -> (->) r b
중위 연산자 형태로 나타내면 아래와 같이 되겠죠.
fmap :: (a -> b) -> (r -> a) -> (r -> b)
function Functor
fmap :: (a -> b) -> (r -> a) -> (r -> b)
fmap f g = (\x -> f (g x))
다시 fmap으로 돌아가면, f의 타입은 a -> b, g의 타입은 r -> a이고, 리턴 타입은 r -> b가
되어야한다는 것을 알 수 있습니다. 그래서 결과로 나오는 함수는 (\x -> f (g x)), 즉 f와 g의
합성함수입니다. a -> b와 r -> a를 합성하면 r -> b타입의 함수가 되죠. 즉 (->) r에 대한
Functor인스턴스는 아래와 같이 구현할 수도 있다는 거죠.
instance Functor ((->) r) where
fmap = (.)
function Functor
사용 예제를 살펴봅시다.
Prelude> :t fmap (*3) (+100)
fmap (*3) (+100) :: (Num a) => a -> a
Prelude> fmap (*3) (+100) 1
303
Prelude> (*3) `fmap` (+100) $ 1
303
Prelude> (*3) . (+100) $ 1
303
Prelude> fmap (show . (*3)) (*100) 1
"300"
function Applicative
이번엔 함수 타입에 대한 Applicative Functor 구현을 살펴봅시다.
instance Applicative ((->) r) where
pure x = (\_ -> x)
f <*> g = \x -> f x (g x)
pure는 단순합니다. 원래 pure의 타입 서명은 a -> f a고, 이 때 f가 (-> r)이므로 여기서
pure의 타입은 a -> (r -> a)가 되어야겠죠. 그래서 어떤 r타입의 인자를 받은 다음 그 인자와
상관없이 원래의 a값을 돌려주는 함수(항등 함수)가 (->) r에 대한 pure 구현이 됩니다.
Prelude> (pure 3) "abcd"
3
function Applicative
<*> 함수는 조금 복잡합니다. 역시 타입 서명부터 차근차근 살펴봅시다.
(<*>) :: f (a -> b) -> f a -> f b
이 때 f를 현재 구현에 맞게 (-> r)로 바꿔서 생각하면,
(<*>) :: (r -> a -> b) -> (r -> a) -> (r -> b)
f <*> g = \x -> f x (g x)
즉, <*> 함수는 어떤 r 타입의 인자 x를 받아서 함수 f에 x와 g x를 인자로 넘긴 결과를 반환하는 새로운
함수를 리턴하는거죠. 타입 서명을 기준으로 하나씩 짚어보시면 이해하기 쉬울 거에요. 이제 이 걸
실제로 어떻게 사용할 수 있는 지 살펴봅시다.
function Applicative
Prelude> :t (+) <$> (+3) <*> (*100)
(+) <$> (+3) <*> (*100) :: (Num a) => a -> a
Prelude> (+) <$> (+3) <*> (*100) $ 5
508
Applicative Functor에서의 동작은, 맨 마지막 인자를 그 앞의 함수들에 인자로 넘긴 다음 그 결과에
대해 맨 처음 함수를 호출한다고 생각하시면 이해하기 편합니다.
(+) <$> (+3) <*> (*100) $ 5
(+) (5+3) (5*100)
(+) 8 500
508
function Applicative
좀 더 복잡한 예제를 봅시다.
Prelude> (\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5
[8.0, 10.0, 2.5)
이 역시 아까 전과 마찬가지로, <$> 뒤쪽 함수들에 대해 마지막 인자($ 뒤의 값)를 적용시킨 뒤 그
결과를 다시 맨 처음 함수에 인자로 넘긴 것이 최종 결과가 됩니다.
(\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5
(\x y z -> [x,y,z]) (5+3) (5*2) (5/2)
(\x y z -> [x,y,z]) 8 10 2.5
[8.0, 10.0, 2.5]
function Monad
함수 타입에 대한 Monad는 Reader Monad라고 불립니다. 우선 실제 구현부터 살펴봅시다.
instance Monad ((->) r) where
return x = \_ -> x
f >>= k = \r -> k (f r) r
return 은 거의 일반적으로 그렇듯이 Applicative의 pure와 구현이 같은 것을 알 수 있죠. >>=
연산을 다시 타입부터 하나씩 짚어나가 봅시다.
function Monad
원래 >>= 함수의 타입은 (Monad m) => m a -> ( a -> m b) -> m b죠. 여기서 지금 m이
(->) r인 경우에 대해 다루고 있으니, >>=의 타입은
(r -> a) -> (a -> r -> b) -> (r -> b)가 될 겁니다.
f >>= k = \r -> k (f r) r
여기서 f의 타입은 r->a, k의 타입은 a->r->b죠. 따라서 어떤 인자 r을 받고, 그 r을 f에 넘긴 결과
값(a타입)과 r 값 자체를 k에 넘기면 r->b인 함수가 나오게 됩니다. 그럼 실제 적용 예를 봅시다.
동작하는 느낌 자체는 Applicative Functor에서와 굉장히 유사합니다.
function Monad
addStuff :: Int -> Int
addStuff = do
a <- (*2)
b <- (+10)
return (a+b)
Prelude> addStuff 3
19
Applicative Functor때 썼던 예제와 굉장히 유사한 예제입니다. 동작 결과도 비슷하죠. a, b는
addStuff가 인자로 받은 Int 값을 주어진 함수( (*2), (+10) )에 적용한 결과 값이 됩니다. 그리고 그
결과로 a와 b를 더한 값을 리턴하죠.
function Monad
addStuff :: Int -> Int
addStuff = do
a <- (*2)
b <- (+10)
return (a+b)
이 코드의 동작 과정을 잘 살펴보면 a에 바인딩된 (*2), b에 바인딩된 (+10)과 같은 함수들은
모두 addStuff 함수에서 인자로 넘어온 값을 인자로 하여 호출됩니다. 즉, Reader Monad에서
바인딩되는 모든 함수들은 처음에 함수가 인자로 받은 인자 값을 공유하게 된다는 거죠. 이 특성을
이용하여 여러 함수가 같은 환경 변수를 공유하는 상황에 Reader Monad를 효율적으로 쓸 수
있습니다.
응용
data Config = Config { level :: Int, name :: String }
job :: Config -> String
job config = "level is " ++ l ++ n
where l = levelJob config
n = nameJob config
levelJob :: Config -> String
levelJob config = if l > 5 then "high" else "low"
where l = level config
nameJob :: Config -> String
nameJob config = ",name is " ++ name config
응용
우선 실행 결과입니다.
Prelude> job $ Config 3 "abc"
"level is low, name is abc"
Prelude> job $ Config 7 "def"
"level is high, name is def"
앞의 코드는 특정한 Config 값이 있고, 그 Config 값을 바탕으로 동작하는 함수들이 있는 간단한
예제입니다. 여러 개의 함수가 동일한 Config 값을 가지고 동작하기 때문에 이 함수들은 모두 Config
값을 인자로 받아야하고, 그 때문에 함수를 호출할 때 일일히 config 값을 인자로 넘겨줘야하는
번거로움이 있습니다. 이런 상황에 function monad(reader monad)를 사용하면 코드를 더
간결하게 작성할 수 있습니다. 앞의 코드를 모나드를 이용해 다시 작성해봅시다.
응용
import Control.Monad.Reader
data Config = Config
{ level :: Int
, name :: String }
job :: Config -> String
job = do
l <- levelJob
n <- nameJob
return $ "level is " ++ l ++ n
levelJob :: Config -> String
levelJob = do
l <- level
return $ if l > 5
then "high"
else "low"
nameJob :: Config -> String
nameJob = do
n <- name
return $ ",name is" ++ n