SlideShare a Scribd company logo
1 of 21
Download to read offline
Haskell
Study
15. monad transformer
Nested Monad
코드를 짜다보면 모나드 안에 모나드가 중첩되는 경우가 생각보다 빈번하게 발생합니다. 단순히
생각해봐도, 'stateful한 연산을 하면서 로그를 기록하고 싶다'라고 한다면 어떻게 해야할까요?
이건 State Monad와 Writer Monad를 동시에 사용해야만 하는 케이스입니다. 이런 경우에는
부득이하게 모나드가 중첩되어서 들어갈 수 밖에 없죠. 그리고 이렇게 모나드가 중첩되면 코드가
상당히 지저분해집니다. 이럴 때 좋은 해결책이 될 수 있는 것이 바로 Monad Transformer
입니다. 새로운 개념을 배울 때는 역시 예제부터 시작하는 것이 좋으니 이번에도 간단한 예제부터
시작해봅시다.
Nested Monad
isValid "Red" = True
isValid "Blue" = True
isValid _ = False
check :: IO (Maybe String)
check = do
s <- getLine
return $ if isValid s
then Just s
else Nothing
문자열을 하나 입력받아 이게 정당한 조건을 만족한다면 Just s, 아니면 Nothing을 반환하는
함수입니다. 이 함수는 실패할 수도 있으면서 IO기도 하기 때문에 IO (Maybe String)타입을 가지죠.
Nested Monad
twoCheck :: IO ()
twoCheck = do
c1 <- check
case c1 of
Just val -> do
c2 <- check
case c2 of
Just val2 ->
if val == val2
then putStrLn "Equal"
else putStrLn "Not Equal"
Nothing -> putStrLn "invalid Input"
Nothing -> putStrLn "invalid Input"
이 함수는 check 함수를 두 번 호출해서, 그 내부의 값에
따라 서로 다른 문자열을 화면에 출력하는 함수입니다. 예
전에 처음 Maybe 모나드에 대해 언급했을 때와 마찬가지
로, 실패할 수 있는 모든 케이스를 다 확인해야하기 때문에
코드가 굉장히 지저분합니다. 그래서 Maybe Monad를
이용하고 싶지만 모나드가 중첩되어 있어 적용하기가 쉽지
않죠. 이런 경우에 Monad Transformer를 사용할 수 있
습니다.
MaybeT
Control.Monad.Trans.Maybe 모듈에는 MaybeT 라는 타입이 선언되어있습니다.
newtype MaybeT m a = MaybeT { runMaybeT m (Maybe a) }
MaybeT 자체는 단순히 newtype을 이용한 래핑이라는 것을 알 수 있습니다. 맨 안쪽에 Maybe
모나드가 들어가 있고, 그 모나드를 바깥에서 다른 모나드 m이 감싸고 있는 형태의 타입을
MaybeT m a로 정의한 거죠. 아까 저희가 정의한 함수 check의 타입은 IO (Maybe String)
이었는데, 이를 MaybeT 형태로 나타내면 MaybeT IO String이 됩니다. 이 자체로는 물론 아무런
의미가 없겠지만, 이 newtype에 대한 모나드 인스턴스가 정의되어있고 이를 이용해 중첩된 모나드를
마치 하나의 모나드인 것처럼 사용할 수 있게 됩니다.
MaybeT
instance Monad m => Monad (MaybeT m) where
	 return = MaybeT . return . Just
	x >>= f = MaybeT $ do
		maybe_value <- runMaybeT x
		case maybe_value of
			Nothing -> return Nothing
			Just value -> runMaybeT $ f value
Maybe T m에 대한 모나드 타입 클래스는 다음과 같이 정의되어있습니다. 이제 이 코드를 하나씩
따라가면서 Monad Transformer가 어떤 원리로 작동하는지 이해해봅시다.
MaybeT
return = MaybeT . return . Just
return은 단순합니다. 주어진 값을 Just 값 생성자를 이용해 Maybe 값으로 만들고, 이걸 다시
return 함수를 통해 Maybe를 감싸고 있는 바깥 모나드 속으로 집어넣습니다. 이 시점에서 타입은
m (Maybe a) 가 되겠죠. 이걸 다시 MaybeT 값 생성자를 이용해 MaybeT 타입의 값으로 만듭니다.
외부 모나드로 한 번 감싸준다는 것을 제외하고는 Maybe 모나드의 원래 return 구현과 크게 다를 게
없죠.
MaybeT
x >>= f = MaybeT $ do
	maybe_value <- runMaybeT x
	 case maybe_value of
		Nothing -> return Nothing
		Just value -> runMaybeT $ f value
핵심인 >>= 함수입니다. 항상 타입을 기준으로 내용을 이해하면 쉽다고 말했죠. 여기서 >>= 함수의
타입은 MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b 입니다.
함수 구현부에서 do 표기법은 안쪽의 Maybe를 감싸고 있는 m 모나드에 대한 것입니다. 여기서
runMaybeT x를 통해 모나드 안쪽에 있는 Maybe 값을 가져오죠(maybe_value). 그리고 그 값이
Nothing이라면 return Nothing을 통해 Nothing값을 m 모나드로 감싸고, Just value라면 그
value 값을 f 함수에 넘겨서 나온 MaybeT 값을 돌려주죠. 역시, 외부를 m 모나드로 한 번 감싸는
것을 제외하곤 Maybe 함수의 구현과 별 다를 바가 없습니다.
MaybeT
그리고 이에 부가적으로 중요한 MonadTrans 타입 클래스가 있습니다. 이 타입클래스는 Monad
Transformer와 관련된 타입클래스로, lift라는 함수를 제공합니다. lift 함수의 타입은 다음과
같습니다.
lift :: (MonadTrans t, Monad m) => m a -> t m a
이름처럼 그냥 모나드 값 (m a)을 모나드 트랜스포머(t) 내부로 들어올리는(lift) 함수입니다.
이 함수를 이용해서 외부에 있는 모나드 m과 관련된 함수를 따로 정의하거나 할 필요없이 모나드
트랜스포머 내부에서도 사용할 수 있게 됩니다.
MaybeT
instance MonadTrans MaybeT where
	lift = MaybeT . (liftM Just)
MaybeT 모나드 트랜스포머의 경우 lift 함수는 위와 같이 정의되어있습니다. 여기서 liftM
함수는 이전 슬라이드에서 다뤘듯이 단순히 fmap으로 생각하시면 됩니다. MaybeT의 경우 Maybe
외부의 함수 적용 결과는 내부로 들어올릴 때 단순히 Just만 적용해주면 문맥상 문제가 없겠죠
(Maybe 외부의 값을 들어올리는 것이므로 이 값은 Maybe의 문맥과는 상관이 없습니다. 따라서 Just
를 이용해 단순히 값만 Maybe 모나드 내부로 가져옵니다).
MaybeT
이제 MaybeT를 이용해 앞서 짰던 코드를 다시 작성해봅시다. isValid 함수는 달라질 게 없으니 check
함수와 twoCheck 함수만 다시 작성해시다. 우선 check 함수입니다.
check :: MaybeT IO String
check = do
s <- lift getLine
guard (isValid s)
return s
Monad 파트에서 할 때 다뤘던 guard 함수를 이용해 조건에 맞지 않으면 MaybeT (IO Nothing)이
반환되고, 그 외의 경우에는 MaybeT (IO (Just s)) 값이 반환되게 만들었습니다.
MaybeT
twoCheck :: MaybeT IO ()
twoCheck = do
c1 <- check
c2 <- check
if c1 == c2
then lift $ putStrLn "Equal"
else lift $ putStrLn "Not Equal"
twoCheck 함수는 정말 간결해졌습니다. IO와 Maybe 모나드가 동시에 사용되고 있지만, 제일 안
쪽에 있는 모나드인 Maybe 모나드만 쓰듯이 사용할 수 있게 됩니다. 물론 lift 함수를 이용해서 IO
모나드와 관련된 함수도 손쉽게 쓸 수 있죠.
WriterT
이번엔 WriterT 모나드 트랜스포머에 대해 알아봅시다. 기본적으로 모나드 트랜스포머들은 기존의
모나드들을 다른 모나드들과 중첩시키기 위해 존재한다고 생각하시면 됩니다. 그리고 Writer, State
등등의 모나드의 경우 원래는 Writer와 WriterT 가 따로 존재했으나, 모나드 트랜스포머의 개념이
일반화되며 Writer는 단순히 아래와 같이 정의되게 변경되었습니다(State도 비슷합니다).
type Writer w = WriterT w Identity
Identity 모나드는 컨텍스트가 없는 모나드입니다(아무런 컨텍스트가 없는 기본 값에서의 연산과
동일하다고 생각하시면 됩니다). 즉, Writer 모나드는 WriterT 모나드 트랜스포머 중 Writer
모나드를 둘러 싼 바깥 모나드가 없는 경우(Identity)를 나타내는 것 뿐이라는 거죠. 그래서 WriterT
모나드 트랜스포머 값을 만들기 위해 writer 함수를 사용합니다.
writer :: Monad m => (a,w) -> WriterT w m a
WriterT
그럼 이번엔 WriterT 모나드를 사용한 예제를 살펴봅시다. 이전엔 모나드 2개가 중첩되는 케이스를
살펴봤으니 이번엔 모나드 3개가 중첩되는 케이스를 보죠. 일단 여기서 하고 싶은 작업은 아래와
같습니다.
1. 숫자 2개를 입력받는다.
2. 그 숫자 2개를 곱한 결과를 구한다.
여기서, 숫자가 입력되었는지 아닌지를 확인하기 위해 Maybe, 그리고 입력을 위해 IO, 작업과정의
로깅을 위해 Writer를 사용할 겁니다.
WriterT
우선 로그를 찍기 위한 logNumber 함수와 주어진 입력값이 정당한지 아닌지 판별할 때 쓸 isValid
함수의 구현부터 봅시다.
logNumber :: (Monad m) => Int -> WriterT [String] m Int
logNumber x = writer (x, ["Got Number: " ++ show x])
isValid :: String -> Bool
isValid = all (`elem` "0123456789")
isValid는 따로 설명이 필요없을 만큼 간단합니다. logNumber의 경우, 숫자 값 하나를 입력받아
WriterT [String] m Int 값을 반환합니다. 일반화된 로그 함수라고 생각할 수 있죠. logNumber
는 외부를 특정 모나드 m이 감싸고 있을 때, 그 내부 Writer 모나드에서 로그 작업을 수행하는
함수입니다.
WriterT
readNum :: MaybeT (WriterT [String] IO) Int
readNum = do
str <- liftIO getLine
guard (isValid str)
let num = read str
lift $ logNumber num
return num
readNum 함수는 이전 슬라이드의 두 함수를 이용해 실제로 값을 입력받습니다. 제일 안쪽 모나드가
Maybe, 그 바깥이 Writer, 그 바깥이 IO 모나드의 형태가 되죠. liftIO 함수는 IO 작업 결과를 맨
안쪽 모나드로 끌어 올려주는 역할을 합니다. 나머지 동작은 여태껏 해왔던 것과 유사하죠. valid하지
않으면 Nothing, valid하면 값을 읽어서 기록하고 그 값을 돌려줍니다.
WriterT
multiply :: MaybeT (WriterT [String] IO) Int
multiply = do
a <- readNum
b <- readNum
lift $ tell [show a ++ " * " ++ show b ++ " = " ++ show (a*b)]
return $ a*b
multiply 함수는 실제로 곱셈을 수행하는 함수입니다. 이 함수는 두 숫자를 입력받은 후 곱셈한
결과를 로깅하고, 그 결과값을 리턴해줍니다.
WriterT
main = do
(num, log) <- runWriterT $ runMaybeT multiply
mapM putStrLn log
실제로 multiply 함수를 사용하는 예제입니다. main 함수에서 multiply 함수를 이용해 곱셈과
로깅을 수행하고, 그 값을 runMaybeT와 runWriterT를 통해 맨 바깥 IO 모나드까지 꺼내옵니다.
그리고 그 과정에서 얻은 로그 값을 mapM 함수를 이용해 화면에 출력합니다. mapM 함수는 map
함수의 모나드 버젼이라고 생각하시면 됩니다.
mapM :: Monad m => ( a -> m b ) -> [a] -> [m b]
StateT
마지막으로 StateT 모나드에 대해서 살펴봅시다. StateT 모나드 역시 앞에서 다룬 것들과 크게
차이나지 않습니다. 이전에 구현했던 stack을 Writer 모나드를 이용해 push / pop할 때 로그까지
남기도록 아래와 같이 작성할 수 있습니다.
push :: (Show a, Monad m) => a -> StateT [a] (WriterT [String] m) ()
push val = StateT $ s ->
writer (((), val:s), ["push" ++ show val])
타입이 조금 복잡해보이긴 하지만, 기존과 크게 다르진 않습니다. StateT 값 생성자의 타입은
StateT :: (s -> m (a, s)) -> StateT s m a 인데, stateful한 연산이 일어나는 State
모나드 바깥에 다른 모나드가 더 있다고 생각하면 왜 이런 타입을 가져야하는 지 이해하기 조금 쉬울
겁니다.
StateT
pop :: (Show a, Monad m) => StateT [a] (WriterT [String] m) (Maybe a)
pop = StateT popFunc where
popFunc (x:xs) = writer ((Just x, xs), ["pop" ++ show x])
popFunc xs = writer ((Nothing, xs), ["stack is empty"])
pop은 스택이 비어있을 때를 다루기 위해 Maybe a 값을 리턴하게 만들어보았습니다. 스택이 비어
있다면 스택이 비었다는 로그를 남기며 Nothing을 돌려주고, 스택이 비지 않았다면 pop하면서 pop
한 원소가 무엇인지에 관한 로그를 같이 남겨주죠.
StateT
stackIO :: StateT [String] (WriterT [String] IO) ()
stackIO = do
a <- liftIO getLine
b <- liftIO getLine
push a
push b
r1 <- pop
r2 <- pop
r3 <- pop
lift $ tell [show (r1,r2,r3)]
사용 예제입니다. 이전에 State 모나드에서 썼던 예제와 코드 구조가 크게 다르지 않습니다. 타입은 좀
많이 복잡해졌지만요..

More Related Content

What's hot

Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...
Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...
Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...Philip Schwarz
 
Python Lambda Function
Python Lambda FunctionPython Lambda Function
Python Lambda FunctionMd Soyaib
 
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1Philip Schwarz
 
Angular 4 and TypeScript
Angular 4 and TypeScriptAngular 4 and TypeScript
Angular 4 and TypeScriptAhmed El-Kady
 
Java script final presentation
Java script final presentationJava script final presentation
Java script final presentationAdhoura Academy
 
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2Philip Schwarz
 
The Uniform Access Principle
The Uniform Access PrincipleThe Uniform Access Principle
The Uniform Access PrinciplePhilip Schwarz
 
Nodejs functions & modules
Nodejs functions & modulesNodejs functions & modules
Nodejs functions & modulesmonikadeshmane
 
‘go-to’ general-purpose sequential collections - from Java To Scala
‘go-to’ general-purpose sequential collections -from Java To Scala‘go-to’ general-purpose sequential collections -from Java To Scala
‘go-to’ general-purpose sequential collections - from Java To ScalaPhilip Schwarz
 
ES6 presentation
ES6 presentationES6 presentation
ES6 presentationritika1
 
Computer Graphics in Java and Scala - Part 1
Computer Graphics in Java and Scala - Part 1Computer Graphics in Java and Scala - Part 1
Computer Graphics in Java and Scala - Part 1Philip Schwarz
 

What's hot (20)

Haskell study 4
Haskell study 4Haskell study 4
Haskell study 4
 
Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...
Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...
Refactoring: A First Example - Martin Fowler’s First Example of Refactoring, ...
 
Clojure Monad
Clojure MonadClojure Monad
Clojure Monad
 
Python Lambda Function
Python Lambda FunctionPython Lambda Function
Python Lambda Function
 
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 1
 
Haskell study 5
Haskell study 5Haskell study 5
Haskell study 5
 
Angular 4 and TypeScript
Angular 4 and TypeScriptAngular 4 and TypeScript
Angular 4 and TypeScript
 
AngularJS
AngularJSAngularJS
AngularJS
 
Nodejs vatsal shah
Nodejs vatsal shahNodejs vatsal shah
Nodejs vatsal shah
 
Haskell study 1
Haskell study 1Haskell study 1
Haskell study 1
 
Python oop class 1
Python oop   class 1Python oop   class 1
Python oop class 1
 
Java script final presentation
Java script final presentationJava script final presentation
Java script final presentation
 
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2
Scala 3 by Example - Algebraic Data Types for Domain Driven Design - Part 2
 
JavaScript Basics
JavaScript BasicsJavaScript Basics
JavaScript Basics
 
The Uniform Access Principle
The Uniform Access PrincipleThe Uniform Access Principle
The Uniform Access Principle
 
File system node js
File system node jsFile system node js
File system node js
 
Nodejs functions & modules
Nodejs functions & modulesNodejs functions & modules
Nodejs functions & modules
 
‘go-to’ general-purpose sequential collections - from Java To Scala
‘go-to’ general-purpose sequential collections -from Java To Scala‘go-to’ general-purpose sequential collections -from Java To Scala
‘go-to’ general-purpose sequential collections - from Java To Scala
 
ES6 presentation
ES6 presentationES6 presentation
ES6 presentation
 
Computer Graphics in Java and Scala - Part 1
Computer Graphics in Java and Scala - Part 1Computer Graphics in Java and Scala - Part 1
Computer Graphics in Java and Scala - Part 1
 

Viewers also liked

Next 게임 실전 프로젝트 슬라이드
Next 게임 실전 프로젝트 슬라이드Next 게임 실전 프로젝트 슬라이드
Next 게임 실전 프로젝트 슬라이드Nam Hyeonuk
 
Iocp 기본 구조 이해
Iocp 기본 구조 이해Iocp 기본 구조 이해
Iocp 기본 구조 이해Nam Hyeonuk
 
하스켈 프로그래밍 입문 3
하스켈 프로그래밍 입문 3하스켈 프로그래밍 입문 3
하스켈 프로그래밍 입문 3Kwang Yul Seo
 
(2015 06-16) Three Approaches to Monads
(2015 06-16) Three Approaches to Monads(2015 06-16) Three Approaches to Monads
(2015 06-16) Three Approaches to MonadsLawrence Evans
 
Age Of Empires II : Age Of Kings Postmotem
Age Of Empires II : Age Of Kings PostmotemAge Of Empires II : Age Of Kings Postmotem
Age Of Empires II : Age Of Kings PostmotemNam Hyeonuk
 
Functional Programming by Examples using Haskell
Functional Programming by Examples using HaskellFunctional Programming by Examples using Haskell
Functional Programming by Examples using Haskellgoncharenko
 
전자책산업동향과 서비스 모델 (Slide Share)
전자책산업동향과 서비스 모델 (Slide Share)전자책산업동향과 서비스 모델 (Slide Share)
전자책산업동향과 서비스 모델 (Slide Share)Joong Ho Lee
 
Effective c++ chapter 1,2 요약
Effective c++ chapter 1,2 요약Effective c++ chapter 1,2 요약
Effective c++ chapter 1,2 요약Nam Hyeonuk
 
4. 함수포인터
4. 함수포인터4. 함수포인터
4. 함수포인터Hoyoung Jung
 
포인터의 공식
포인터의 공식포인터의 공식
포인터의 공식Hoyoung Jung
 
포인터의기초 (2) - 포인터 사용하기1
포인터의기초 (2) - 포인터 사용하기1포인터의기초 (2) - 포인터 사용하기1
포인터의기초 (2) - 포인터 사용하기1Hoyoung Jung
 
Real-World Functional Programming @ Incubaid
Real-World Functional Programming @ IncubaidReal-World Functional Programming @ Incubaid
Real-World Functional Programming @ IncubaidNicolas Trangez
 
Memory & object pooling
Memory & object poolingMemory & object pooling
Memory & object poolingNam Hyeonuk
 
TCP/IP Protocol - JAVA
TCP/IP Protocol - JAVATCP/IP Protocol - JAVA
TCP/IP Protocol - JAVAcooddy
 
Effective C++ Chaper 1
Effective C++ Chaper 1Effective C++ Chaper 1
Effective C++ Chaper 1연우 김
 
네트워크 스터디(Tcp 소켓 프로그래밍)
네트워크 스터디(Tcp 소켓 프로그래밍)네트워크 스터디(Tcp 소켓 프로그래밍)
네트워크 스터디(Tcp 소켓 프로그래밍)MoonLightMS
 

Viewers also liked (20)

Next 게임 실전 프로젝트 슬라이드
Next 게임 실전 프로젝트 슬라이드Next 게임 실전 프로젝트 슬라이드
Next 게임 실전 프로젝트 슬라이드
 
Multi thread
Multi threadMulti thread
Multi thread
 
Iocp 기본 구조 이해
Iocp 기본 구조 이해Iocp 기본 구조 이해
Iocp 기본 구조 이해
 
하스켈 프로그래밍 입문 3
하스켈 프로그래밍 입문 3하스켈 프로그래밍 입문 3
하스켈 프로그래밍 입문 3
 
(2015 06-16) Three Approaches to Monads
(2015 06-16) Three Approaches to Monads(2015 06-16) Three Approaches to Monads
(2015 06-16) Three Approaches to Monads
 
Age Of Empires II : Age Of Kings Postmotem
Age Of Empires II : Age Of Kings PostmotemAge Of Empires II : Age Of Kings Postmotem
Age Of Empires II : Age Of Kings Postmotem
 
Functional Programming by Examples using Haskell
Functional Programming by Examples using HaskellFunctional Programming by Examples using Haskell
Functional Programming by Examples using Haskell
 
전자책산업동향과 서비스 모델 (Slide Share)
전자책산업동향과 서비스 모델 (Slide Share)전자책산업동향과 서비스 모델 (Slide Share)
전자책산업동향과 서비스 모델 (Slide Share)
 
Effective c++ chapter 1,2 요약
Effective c++ chapter 1,2 요약Effective c++ chapter 1,2 요약
Effective c++ chapter 1,2 요약
 
Haskell study 0
Haskell study 0Haskell study 0
Haskell study 0
 
4. 함수포인터
4. 함수포인터4. 함수포인터
4. 함수포인터
 
포인터의 공식
포인터의 공식포인터의 공식
포인터의 공식
 
포인터의기초 (2) - 포인터 사용하기1
포인터의기초 (2) - 포인터 사용하기1포인터의기초 (2) - 포인터 사용하기1
포인터의기초 (2) - 포인터 사용하기1
 
3.포인터
3.포인터3.포인터
3.포인터
 
Real-World Functional Programming @ Incubaid
Real-World Functional Programming @ IncubaidReal-World Functional Programming @ Incubaid
Real-World Functional Programming @ Incubaid
 
Memory & object pooling
Memory & object poolingMemory & object pooling
Memory & object pooling
 
TCP/IP Protocol - JAVA
TCP/IP Protocol - JAVATCP/IP Protocol - JAVA
TCP/IP Protocol - JAVA
 
Database
DatabaseDatabase
Database
 
Effective C++ Chaper 1
Effective C++ Chaper 1Effective C++ Chaper 1
Effective C++ Chaper 1
 
네트워크 스터디(Tcp 소켓 프로그래밍)
네트워크 스터디(Tcp 소켓 프로그래밍)네트워크 스터디(Tcp 소켓 프로그래밍)
네트워크 스터디(Tcp 소켓 프로그래밍)
 

More from Nam Hyeonuk

Haskell study 11
Haskell study 11Haskell study 11
Haskell study 11Nam Hyeonuk
 
Haskell study 10
Haskell study 10Haskell study 10
Haskell study 10Nam Hyeonuk
 
Tcp ip & io model
Tcp ip & io modelTcp ip & io model
Tcp ip & io modelNam Hyeonuk
 
구문과 의미론(정적 의미론까지)
구문과 의미론(정적 의미론까지)구문과 의미론(정적 의미론까지)
구문과 의미론(정적 의미론까지)Nam Hyeonuk
 
Stl vector, list, map
Stl vector, list, mapStl vector, list, map
Stl vector, list, mapNam Hyeonuk
 

More from Nam Hyeonuk (9)

Haskell study 11
Haskell study 11Haskell study 11
Haskell study 11
 
Haskell study 10
Haskell study 10Haskell study 10
Haskell study 10
 
Haskell study 7
Haskell study 7Haskell study 7
Haskell study 7
 
Exception&log
Exception&logException&log
Exception&log
 
Iocp advanced
Iocp advancedIocp advanced
Iocp advanced
 
Tcp ip & io model
Tcp ip & io modelTcp ip & io model
Tcp ip & io model
 
구문과 의미론(정적 의미론까지)
구문과 의미론(정적 의미론까지)구문과 의미론(정적 의미론까지)
구문과 의미론(정적 의미론까지)
 
Gpg 1.1
Gpg 1.1Gpg 1.1
Gpg 1.1
 
Stl vector, list, map
Stl vector, list, mapStl vector, list, map
Stl vector, list, map
 

Haskell study 15

  • 2. Nested Monad 코드를 짜다보면 모나드 안에 모나드가 중첩되는 경우가 생각보다 빈번하게 발생합니다. 단순히 생각해봐도, 'stateful한 연산을 하면서 로그를 기록하고 싶다'라고 한다면 어떻게 해야할까요? 이건 State Monad와 Writer Monad를 동시에 사용해야만 하는 케이스입니다. 이런 경우에는 부득이하게 모나드가 중첩되어서 들어갈 수 밖에 없죠. 그리고 이렇게 모나드가 중첩되면 코드가 상당히 지저분해집니다. 이럴 때 좋은 해결책이 될 수 있는 것이 바로 Monad Transformer 입니다. 새로운 개념을 배울 때는 역시 예제부터 시작하는 것이 좋으니 이번에도 간단한 예제부터 시작해봅시다.
  • 3. Nested Monad isValid "Red" = True isValid "Blue" = True isValid _ = False check :: IO (Maybe String) check = do s <- getLine return $ if isValid s then Just s else Nothing 문자열을 하나 입력받아 이게 정당한 조건을 만족한다면 Just s, 아니면 Nothing을 반환하는 함수입니다. 이 함수는 실패할 수도 있으면서 IO기도 하기 때문에 IO (Maybe String)타입을 가지죠.
  • 4. Nested Monad twoCheck :: IO () twoCheck = do c1 <- check case c1 of Just val -> do c2 <- check case c2 of Just val2 -> if val == val2 then putStrLn "Equal" else putStrLn "Not Equal" Nothing -> putStrLn "invalid Input" Nothing -> putStrLn "invalid Input" 이 함수는 check 함수를 두 번 호출해서, 그 내부의 값에 따라 서로 다른 문자열을 화면에 출력하는 함수입니다. 예 전에 처음 Maybe 모나드에 대해 언급했을 때와 마찬가지 로, 실패할 수 있는 모든 케이스를 다 확인해야하기 때문에 코드가 굉장히 지저분합니다. 그래서 Maybe Monad를 이용하고 싶지만 모나드가 중첩되어 있어 적용하기가 쉽지 않죠. 이런 경우에 Monad Transformer를 사용할 수 있 습니다.
  • 5. MaybeT Control.Monad.Trans.Maybe 모듈에는 MaybeT 라는 타입이 선언되어있습니다. newtype MaybeT m a = MaybeT { runMaybeT m (Maybe a) } MaybeT 자체는 단순히 newtype을 이용한 래핑이라는 것을 알 수 있습니다. 맨 안쪽에 Maybe 모나드가 들어가 있고, 그 모나드를 바깥에서 다른 모나드 m이 감싸고 있는 형태의 타입을 MaybeT m a로 정의한 거죠. 아까 저희가 정의한 함수 check의 타입은 IO (Maybe String) 이었는데, 이를 MaybeT 형태로 나타내면 MaybeT IO String이 됩니다. 이 자체로는 물론 아무런 의미가 없겠지만, 이 newtype에 대한 모나드 인스턴스가 정의되어있고 이를 이용해 중첩된 모나드를 마치 하나의 모나드인 것처럼 사용할 수 있게 됩니다.
  • 6. MaybeT instance Monad m => Monad (MaybeT m) where return = MaybeT . return . Just x >>= f = MaybeT $ do maybe_value <- runMaybeT x case maybe_value of Nothing -> return Nothing Just value -> runMaybeT $ f value Maybe T m에 대한 모나드 타입 클래스는 다음과 같이 정의되어있습니다. 이제 이 코드를 하나씩 따라가면서 Monad Transformer가 어떤 원리로 작동하는지 이해해봅시다.
  • 7. MaybeT return = MaybeT . return . Just return은 단순합니다. 주어진 값을 Just 값 생성자를 이용해 Maybe 값으로 만들고, 이걸 다시 return 함수를 통해 Maybe를 감싸고 있는 바깥 모나드 속으로 집어넣습니다. 이 시점에서 타입은 m (Maybe a) 가 되겠죠. 이걸 다시 MaybeT 값 생성자를 이용해 MaybeT 타입의 값으로 만듭니다. 외부 모나드로 한 번 감싸준다는 것을 제외하고는 Maybe 모나드의 원래 return 구현과 크게 다를 게 없죠.
  • 8. MaybeT x >>= f = MaybeT $ do maybe_value <- runMaybeT x case maybe_value of Nothing -> return Nothing Just value -> runMaybeT $ f value 핵심인 >>= 함수입니다. 항상 타입을 기준으로 내용을 이해하면 쉽다고 말했죠. 여기서 >>= 함수의 타입은 MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b 입니다. 함수 구현부에서 do 표기법은 안쪽의 Maybe를 감싸고 있는 m 모나드에 대한 것입니다. 여기서 runMaybeT x를 통해 모나드 안쪽에 있는 Maybe 값을 가져오죠(maybe_value). 그리고 그 값이 Nothing이라면 return Nothing을 통해 Nothing값을 m 모나드로 감싸고, Just value라면 그 value 값을 f 함수에 넘겨서 나온 MaybeT 값을 돌려주죠. 역시, 외부를 m 모나드로 한 번 감싸는 것을 제외하곤 Maybe 함수의 구현과 별 다를 바가 없습니다.
  • 9. MaybeT 그리고 이에 부가적으로 중요한 MonadTrans 타입 클래스가 있습니다. 이 타입클래스는 Monad Transformer와 관련된 타입클래스로, lift라는 함수를 제공합니다. lift 함수의 타입은 다음과 같습니다. lift :: (MonadTrans t, Monad m) => m a -> t m a 이름처럼 그냥 모나드 값 (m a)을 모나드 트랜스포머(t) 내부로 들어올리는(lift) 함수입니다. 이 함수를 이용해서 외부에 있는 모나드 m과 관련된 함수를 따로 정의하거나 할 필요없이 모나드 트랜스포머 내부에서도 사용할 수 있게 됩니다.
  • 10. MaybeT instance MonadTrans MaybeT where lift = MaybeT . (liftM Just) MaybeT 모나드 트랜스포머의 경우 lift 함수는 위와 같이 정의되어있습니다. 여기서 liftM 함수는 이전 슬라이드에서 다뤘듯이 단순히 fmap으로 생각하시면 됩니다. MaybeT의 경우 Maybe 외부의 함수 적용 결과는 내부로 들어올릴 때 단순히 Just만 적용해주면 문맥상 문제가 없겠죠 (Maybe 외부의 값을 들어올리는 것이므로 이 값은 Maybe의 문맥과는 상관이 없습니다. 따라서 Just 를 이용해 단순히 값만 Maybe 모나드 내부로 가져옵니다).
  • 11. MaybeT 이제 MaybeT를 이용해 앞서 짰던 코드를 다시 작성해봅시다. isValid 함수는 달라질 게 없으니 check 함수와 twoCheck 함수만 다시 작성해시다. 우선 check 함수입니다. check :: MaybeT IO String check = do s <- lift getLine guard (isValid s) return s Monad 파트에서 할 때 다뤘던 guard 함수를 이용해 조건에 맞지 않으면 MaybeT (IO Nothing)이 반환되고, 그 외의 경우에는 MaybeT (IO (Just s)) 값이 반환되게 만들었습니다.
  • 12. MaybeT twoCheck :: MaybeT IO () twoCheck = do c1 <- check c2 <- check if c1 == c2 then lift $ putStrLn "Equal" else lift $ putStrLn "Not Equal" twoCheck 함수는 정말 간결해졌습니다. IO와 Maybe 모나드가 동시에 사용되고 있지만, 제일 안 쪽에 있는 모나드인 Maybe 모나드만 쓰듯이 사용할 수 있게 됩니다. 물론 lift 함수를 이용해서 IO 모나드와 관련된 함수도 손쉽게 쓸 수 있죠.
  • 13. WriterT 이번엔 WriterT 모나드 트랜스포머에 대해 알아봅시다. 기본적으로 모나드 트랜스포머들은 기존의 모나드들을 다른 모나드들과 중첩시키기 위해 존재한다고 생각하시면 됩니다. 그리고 Writer, State 등등의 모나드의 경우 원래는 Writer와 WriterT 가 따로 존재했으나, 모나드 트랜스포머의 개념이 일반화되며 Writer는 단순히 아래와 같이 정의되게 변경되었습니다(State도 비슷합니다). type Writer w = WriterT w Identity Identity 모나드는 컨텍스트가 없는 모나드입니다(아무런 컨텍스트가 없는 기본 값에서의 연산과 동일하다고 생각하시면 됩니다). 즉, Writer 모나드는 WriterT 모나드 트랜스포머 중 Writer 모나드를 둘러 싼 바깥 모나드가 없는 경우(Identity)를 나타내는 것 뿐이라는 거죠. 그래서 WriterT 모나드 트랜스포머 값을 만들기 위해 writer 함수를 사용합니다. writer :: Monad m => (a,w) -> WriterT w m a
  • 14. WriterT 그럼 이번엔 WriterT 모나드를 사용한 예제를 살펴봅시다. 이전엔 모나드 2개가 중첩되는 케이스를 살펴봤으니 이번엔 모나드 3개가 중첩되는 케이스를 보죠. 일단 여기서 하고 싶은 작업은 아래와 같습니다. 1. 숫자 2개를 입력받는다. 2. 그 숫자 2개를 곱한 결과를 구한다. 여기서, 숫자가 입력되었는지 아닌지를 확인하기 위해 Maybe, 그리고 입력을 위해 IO, 작업과정의 로깅을 위해 Writer를 사용할 겁니다.
  • 15. WriterT 우선 로그를 찍기 위한 logNumber 함수와 주어진 입력값이 정당한지 아닌지 판별할 때 쓸 isValid 함수의 구현부터 봅시다. logNumber :: (Monad m) => Int -> WriterT [String] m Int logNumber x = writer (x, ["Got Number: " ++ show x]) isValid :: String -> Bool isValid = all (`elem` "0123456789") isValid는 따로 설명이 필요없을 만큼 간단합니다. logNumber의 경우, 숫자 값 하나를 입력받아 WriterT [String] m Int 값을 반환합니다. 일반화된 로그 함수라고 생각할 수 있죠. logNumber 는 외부를 특정 모나드 m이 감싸고 있을 때, 그 내부 Writer 모나드에서 로그 작업을 수행하는 함수입니다.
  • 16. WriterT readNum :: MaybeT (WriterT [String] IO) Int readNum = do str <- liftIO getLine guard (isValid str) let num = read str lift $ logNumber num return num readNum 함수는 이전 슬라이드의 두 함수를 이용해 실제로 값을 입력받습니다. 제일 안쪽 모나드가 Maybe, 그 바깥이 Writer, 그 바깥이 IO 모나드의 형태가 되죠. liftIO 함수는 IO 작업 결과를 맨 안쪽 모나드로 끌어 올려주는 역할을 합니다. 나머지 동작은 여태껏 해왔던 것과 유사하죠. valid하지 않으면 Nothing, valid하면 값을 읽어서 기록하고 그 값을 돌려줍니다.
  • 17. WriterT multiply :: MaybeT (WriterT [String] IO) Int multiply = do a <- readNum b <- readNum lift $ tell [show a ++ " * " ++ show b ++ " = " ++ show (a*b)] return $ a*b multiply 함수는 실제로 곱셈을 수행하는 함수입니다. 이 함수는 두 숫자를 입력받은 후 곱셈한 결과를 로깅하고, 그 결과값을 리턴해줍니다.
  • 18. WriterT main = do (num, log) <- runWriterT $ runMaybeT multiply mapM putStrLn log 실제로 multiply 함수를 사용하는 예제입니다. main 함수에서 multiply 함수를 이용해 곱셈과 로깅을 수행하고, 그 값을 runMaybeT와 runWriterT를 통해 맨 바깥 IO 모나드까지 꺼내옵니다. 그리고 그 과정에서 얻은 로그 값을 mapM 함수를 이용해 화면에 출력합니다. mapM 함수는 map 함수의 모나드 버젼이라고 생각하시면 됩니다. mapM :: Monad m => ( a -> m b ) -> [a] -> [m b]
  • 19. StateT 마지막으로 StateT 모나드에 대해서 살펴봅시다. StateT 모나드 역시 앞에서 다룬 것들과 크게 차이나지 않습니다. 이전에 구현했던 stack을 Writer 모나드를 이용해 push / pop할 때 로그까지 남기도록 아래와 같이 작성할 수 있습니다. push :: (Show a, Monad m) => a -> StateT [a] (WriterT [String] m) () push val = StateT $ s -> writer (((), val:s), ["push" ++ show val]) 타입이 조금 복잡해보이긴 하지만, 기존과 크게 다르진 않습니다. StateT 값 생성자의 타입은 StateT :: (s -> m (a, s)) -> StateT s m a 인데, stateful한 연산이 일어나는 State 모나드 바깥에 다른 모나드가 더 있다고 생각하면 왜 이런 타입을 가져야하는 지 이해하기 조금 쉬울 겁니다.
  • 20. StateT pop :: (Show a, Monad m) => StateT [a] (WriterT [String] m) (Maybe a) pop = StateT popFunc where popFunc (x:xs) = writer ((Just x, xs), ["pop" ++ show x]) popFunc xs = writer ((Nothing, xs), ["stack is empty"]) pop은 스택이 비어있을 때를 다루기 위해 Maybe a 값을 리턴하게 만들어보았습니다. 스택이 비어 있다면 스택이 비었다는 로그를 남기며 Nothing을 돌려주고, 스택이 비지 않았다면 pop하면서 pop 한 원소가 무엇인지에 관한 로그를 같이 남겨주죠.
  • 21. StateT stackIO :: StateT [String] (WriterT [String] IO) () stackIO = do a <- liftIO getLine b <- liftIO getLine push a push b r1 <- pop r2 <- pop r3 <- pop lift $ tell [show (r1,r2,r3)] 사용 예제입니다. 이전에 State 모나드에서 썼던 예제와 코드 구조가 크게 다르지 않습니다. 타입은 좀 많이 복잡해졌지만요..