haskell study 13

17
Haskell Study 13. function type

Upload: nam-hyeonuk

Post on 17-Jan-2017

338 views

Category:

Software


1 download

TRANSCRIPT

Haskell Study

13. function type

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

응용

앞의 모나드 버젼으로 작성한 코드는 결과는 완전히 동일하지만, 명시적으로 config 값을 인자로

넘겨주는 부분이 전혀 존재하지 않습니다. function monad의 정의에 의해 config 값들은 모두

암시적으로 전달이 되는 거죠. 이런 특성 덕분에 모두가 공유하는 환경 변수 등을 인자로 넘겨야 할

일이 있는 경우 모나드 형태로 작성하면 코드를 훨씬 간결하게 작성할 수 있습니다.