-
egonSchiele
여기, 단순한 값(value)이 있습니다.
Prelude> 2
2
그리고, 우리는 이 값에 함수(function)를 적용하는(apply) 법을 알고 있습니다.
Prelude> (+3) 2
5
아주 간단합니다. 그럼 이제, 이 값이 어떠한 상태(context)안에 들어있다고 가정해봅시다. 이제 여러분은 값을 담을 수 있는 어떠한 상태를, 상자라고 생각할 수 있을 것입니다.
Prelude> Just 2
Just 2
이제, 이 값에 함수를 적용하게 되면, 상태에 따라 다른 결과를 얻게 될 것입니다.
Functors, 이를 기반으로한 개념(idea) 중에는 Applicatives, Monads, Arrows 등이 있습니다. Maybe
데이터 타입은 두가지 상태로 정의할 수 있습니다.
data Maybe a = Nothing | Just a
Prelude> :type Just
Just :: a -> Maybe a
Prelude> :type Nothing
Nothing :: Maybe a
다음으로, Just a
와 Nothing
의 상태에 따라, 함수가 어떻게 적용되는지 보도록 하겠습니다. 우선, Functors에 대해 이야기해 보도록 하겠습니다.
Functors
상자안에 있는 값에는, 단순한 함수를 적용할 수 없습니다.
Prelude> (+3) (Just 2)
ERR - No instance for (Num (Maybe a0))
길거리 출신인 fmap
은 상자에 대해서는 빠삭합니다. fmap
은 상자안에 있는 값에 함수를 어떻게 적용해야 할지를 알고 있습니다. 예를 들어, Just 2
에 (+3)
를 적용시켜 봅시다. fmap
을 사용해보면,
> fmap (+3) (Just 2)
Just 5
쨘! fmap
이 멋지게 해냈습니다. 어떻게 fmap
이 함수를 적용시키는 법을 알고 있는 걸까요?
Functor가 도대체 뭡니까?
Functor
는 typeclass입니다. 여기 정의가 나와있습니다.
Functor
는 fmap을 적용시키는 법을 정의한 데이터 타입입니다. 여기, 어떻게 fmap이 동작하는지 나와 있습니다.
따라서, 우리는 다음과 같이 할 수 있습니다.
> fmap (+3) (Just 2)
Just 5
Maybe
역시 Functor
이기에, fmap
은 마법과도 같이 함수를 적용하였습니다. 다음으로, Just
와 Nothing
에 대해 fmap
을 적용시키는 방법에 대해 나와있습니다.
instance Functor Maybe where
fmap func (Just val) = Just (func val)
fmap func Nothing = Nothing
여기, fmap (+3) (Just 2)
이라고 쳤을때, 어떠한 일이 뒤에서 일어나는지 나와있습니다.
이번에는, Nothing
에 (+3)
을 적용시켜 보도록 하겠습니다.
> fmap (+3) Nothing
Nothing
Maybe functor도 모르는 Bill O’Reilly
메트릭스의 모피어스처럼, fmap
은 무엇을 해야할지 알고 있습니다. Nothing
으로 시작하면, Nothing
으로 끝난다! fmap
은 zen(시작이자 끝)입니다. 이제, Maybe
데이터 타입이 왜 존재하는지 알아봅시다. 예를들어, 다음과 같이 Maybe
가 없는 언어에서 데이터를 가지고 작업을 한다고 한다면,
post = Post.find_by_id(1)
if post
return post.title
else
return nil
end
Haskell은 다음과 같이 할 수 있습니다.
fmap (getPostTitle) (findPost 1)
만약, findPost
가 게시글(post)를 반환한다면, getPostTitle
로 제목(title)을 얻을 것입니다. Nothing
을 반환하면, Nothing
을 얻을 것입니다. 매우 간단하지 않습니까? fmap
의 중위표기(infix) 버전은 <$>
이며, 다음과 같이 쓰기도 합니다.
getPostTitle <$> (findPost 1)
여기, 또 다른 예가 나와있습니다. 리스트에 함수를 적용시키면 무슨일이 생길까요?
리스트 역시 Functor였던 것이였습니다! 여기 정의가 나와있습니다.
instance Functor [] where
fmap = map
자, 자, 마지막으로 하나 더. 함수에 또 다른 함수를 적용시키면 어떤일이 벌어질까요?
fmap (+3) (+1)
Prelude> :type fmap (+3) (+1)
fmap (+3) (+1) :: (Functor ((->) b), Num b) => b -> b
여기 하나의 함수가 있습니다.
함수에 또 다른 함수를 적용해 보도록 하겠습니다.
결과 역시 함수 입니다.
> import Control.Applicative
> let foo = fmap (+3) (+2)
> foo 10
15
따라서, 함수 역시 Functor입니다.
instance Functor ((->) r) where
fmap f g = f . g
함수에 fmap을 사용하면, function composition을 한것과 같습니다.
Applicatives
다음 단계로 넘어가보도록 하겠습니다. Functors에서 나왔던 것처럼, 상자에 값을 넣습니다.
함수 역시 상자에 들어갈 수 있습니다.
그럼 자세히 살펴보도록 하겠습니다. Applicatives는 바보가 아닙니다. Control.Applicative는 상자 속 값에, 상자 속 함수를 적용하는 법을 알도록 <*>
를 정의했습니다.
즉, 다음과 같이 할 수 있습니다.
Just (+3) <*> Just 2 == Just 5
<*>
를 사용하면 조금 흥미로운 결과를 얻을 수 있습니다. 예를들면,
> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]
여기, Functor로는 할 수 없지만, Applicative로 할 수 있는게 나와있습니다. 두개의 인자를 취하는 함수를, 어떻게 속에 값이 들어있는 두개의 상자에 적용시킬까요?
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <$> (Just 4)
ERROR ??? WHAT DOES THIS EVEN MEAN WHY IS THE FUNCTION WRAPPED IN A JUST
Applicatives:
> (+) <$> (Just 5)
Just (+5)
> Just (+5) <*> (Just 3)
Just 8
Functor
을 옆으로 밀쳐내면서 Applicative
이 말합니다.
"어른은 여러 인자들을 다룰 수 있는 함수를 쓰지."
"값이 들어있는 상자와 함수가 들어있는 상자를 <$>
와 <*>
를 이용해서, 값이 들어있는 상자를 얻을 수 있다고! ㅎㅎㅎㅎ!"
> (*) <$> Just 5 <*> Just 3
Just 15
functor가 함수를 적용시키는 것을 지켜보고 있던 applicative
잠깐! 여기 동일한 일을 하는 liftA2
도 있습니다.
> liftA2 (*) (Just 5) (Just 3)
Just 15
Monads
모나드를 배우는 방법 :
- 컴퓨터 공학 박사학위를 딴다.
- 필요없으니 집어 치운다!
Monads는 새로운 해법을 제시하였습니다.
Functors는 상자에 있는 값에 함수를 적용할 수 있습니다.
Applicatives는 상자에 있는 값에, 상자에 있는 함수를 적용 할 수 있습니다.
Monads는 상자에 있는 값에 함수를 적용시켜 값이 들어있는 상자를 반환 할 수 있습니다.
Monads는 이러한 일을 처리하는 ("bind"라 불리는) >>=
라는 함수를 가지고 있습니다.
예제를 살표봅시다. 이제까지 봐왔던 Maybe
는 모나드입니다.
놀고있던 Just a 모나드
짝수에 대해서만 동작하는 함수, half
가 있다고 가정해봅시다.
half x = if even x
then Just (x `div` 2)
else Nothing
값이 들어있는 상자를 넣으면 어떻게 될까요?
함수에 값이 들어있는 상자를 밀어 넣을려면, >>=
가 필요합니다. 여기, >>=
의 사진이 있습니다.
어떻게 동작하는지 확인해 봅시다.
> Just 3 >>= half
Nothing
> Just 4 >>= half
Just 2
> Nothing >>= half
Nothing
내부에서 어떤일이 벌어진 걸까요? Monad
는 또 다른 typeclass입니다. 여기, 정의 중 일부가 나와있습니다.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
>>=
는,
따라서, Maybe
는 Monad입니다:
instance Monad Maybe where
Nothing >>= func = Nothing
Just val >>= func = func val
여기, Just 3
에 대 해 어떻게 동작하는지 나와있습니다!
Nothing
을 넣으면 보다 단순해 집니다.
이렇게 연달아 호출할 수 도 있습니다.
> Just 20 >>= half >>= half >>= half
Nothing
끝내줍니다. 이제 우리는 Maybe
가 Functor이자, Applicative이며, Monad라는 것을 알게 되었습니다.
이제 슬슬, IO
모나드 예제로 가 보겠습니다.
숫자 10아님... 영어 IO임
세개의 특별한 함수가 있습니다. getLine
은 인자를 받지 않고, 사용자의 입력을 받습니다.
getLine :: IO String
readFile
는 문자열(파일명)을 받아, 파일에 있는 내용을 반환합니다.
readFile :: FilePath -> IO String
putStrLn
은 문자열을 받아 출력합니다.
putStrLn :: String -> IO ()
여기 나온 세개의 함수는 평범한 값(혹은 없거나)을 받아서, 값이 들어있는 상자를 반환합니다. 따라서, 우리는 이를 >>=
로 묶을 수 있습니다!
getLine >>= readFile >>= putStrLn
오 예! 모나드 쇼가 펼쳐졌습니다!
덧붙이자면, Haskell은 이 모나드에 대해, do
라는 syntax suger를 제공해 주었습니다.
foo = do
filename <- getLine
contents <- readFile filename
putStrLn contents
Conclusion
- functor는
Functor
타입 클래스를 구현한 데이터 타입이다. - applicative는
Applicative
타입 클래스를 구현한 데이터 타입이다. - monad는
Monad
타입 클래스를 구현한 데이터 타입이다. Maybe
는 이 세개를 모두 구현했으므로, functor이자 applicative이며 monad이다.
이 셋의 차이점은 무엇일까?
- functors:
fmap
이나<$>
를 이용하여 상자 속 값에 함수를 적용할 수 있다. - applicatives:
liftA
나<*>
를 이용하여 상자 속 함수를 상자 속 값에 적용할 수 있다. - monads:
liftM
나>>=
를 이용하여 값이 들어있는 상자를 받아 함수를 적용하고 값이 들어있는 상자를 반환할 수 있다.
우리 모두 모나드라는 것이 쉽고 멋진 개념(트레이드 마크)이라는 것에 대해, 동의한다고 생각합니다. 이 가이드로 만족하기엔 아직 이릅니다. LYAH’s section on Monads를 확인하시기 바랍니다. Miran님 께서 이미 모나드에 관한 깊고, 훌륭한 일을 해냈기에, 저는 많은 것을 얼버무리고 넘어갔습니다.
러시아 버전도 있습니다. 더 많은 모나드와 그림을 원하신다면, three useful monads를 확인하시기 바랍니다.