리더 모나드의 목적은 무엇입니까?


122

리더 모나드는 너무 복잡하고 쓸모없는 것 같습니다. Java 또는 C ++와 같은 명령형 언어에서는 독자 모나드에 해당하는 개념이 없습니다.

간단한 예를 들어 보시고 조금 더 정리해 주시겠습니까?


21
때때로 (수정 불가능한) 환경에서 일부 값을 읽고 싶지만 해당 환경을 명시 적으로 전달하고 싶지 않은 경우 리더 모나드를 사용합니다. Java 또는 C ++에서는 전역 변수를 사용합니다 (정확히 동일하지는 않지만).
Daniel Fischer

5
@Daniel : 그 들리는처럼 엄청 많이 대답
SingleNegationElimination

@TokenMacGuy 대답하기에는 너무 짧고 더 이상 생각하기에는 너무 늦었습니다. 아무도하지 않으면 잠을 자고 나서 할 것입니다.
Daniel Fischer

8
Java 또는 C ++에서 Reader 모나드는 객체의 수명 동안 절대 변경되지 않는 생성자의 객체에 전달 된 구성 매개 변수와 유사합니다. Clojure에서는 매개 변수로 명시 적으로 전달할 필요없이 함수의 동작을 매개 변수화하는 데 사용되는 동적 범위 변수와 비슷합니다.
danidiaz

답변:


169

겁 먹지마! 리더 모나드는 실제로 그렇게 복잡하지 않고 사용하기 쉬운 유틸리티를 가지고 있습니다.

모나드에 접근하는 방법에는 두 가지가 있습니다.

  1. 모나드 무엇을합니까? 어떤 작업을 갖추고 있습니까? 무엇에 좋은가요?
  2. 모나드는 어떻게 구현됩니까? 어디에서 발생합니까?

첫 번째 접근 방식에서 독자 모나드는 추상적 인 유형입니다.

data Reader env a

그런

-- Reader is a monad
instance Monad (Reader env)

-- and we have a function to get its environment
ask :: Reader env env

-- finally, we can run a Reader
runReader :: Reader env a -> env -> a

그렇다면 이것을 어떻게 사용합니까? 글쎄, 리더 모나드는 계산을 통해 (암시 적) 구성 정보를 전달하는 데 좋습니다.

다양한 지점에서 필요한 계산에 "상수"가 있지만 실제로는 다른 값으로 동일한 계산을 수행 할 수 있기를 원할 때마다 리더 모나드를 사용해야합니다.

리더 모나드는 OO 사람들이 의존성 주입 이라고 부르는 작업을 수행하는데도 사용됩니다 . 예를 들어, negamax 알고리즘은 2 인 게임에서 위치 값을 계산하기 위해 자주 (고도로 최적화 된 형태로) 사용됩니다. 그러나 알고리즘 자체는 게임에서 "다음"위치가 무엇인지 결정할 수 있어야하고 현재 위치가 승리 위치인지 알 수 있어야한다는 점을 제외하고는 어떤 게임을하고 있는지 상관하지 않습니다.

 import Control.Monad.Reader

 data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie

 data Game position
   = Game {
           getNext :: position -> [position],
           getState :: position -> GameState
          }

 getNext' :: position -> Reader (Game position) [position]
 getNext' position
   = do game <- ask
        return $ getNext game position

 getState' :: position -> Reader (Game position) GameState
 getState' position
   = do game <- ask
        return $ getState game position


 negamax :: Double -> position -> Reader (Game position) Double
 negamax color position
     = do state <- getState' position 
          case state of
             FirstPlayerWin -> return color
             SecondPlayerWin -> return $ negate color
             Tie -> return 0
             NotOver -> do possible <- getNext' position
                           values <- mapM ((liftM negate) . negamax (negate color)) possible
                           return $ maximum values

그러면 유한하고 결정적인 2 인용 게임에서 작동합니다.

이 패턴은 실제로 의존성 주입이 아닌 것들에도 유용합니다. 금융 분야에서 일한다고 가정 해보면 자산 가격 책정을위한 복잡한 논리를 설계 할 수 있습니다 (파생물 말).이 모든 것은 훌륭하고 악취 나는 모나드 없이도 할 수 있습니다. 그러나 여러 통화를 처리하도록 프로그램을 수정합니다. 즉석에서 통화 간 변환이 가능해야합니다. 첫 번째 시도는 최상위 기능을 정의하는 것입니다.

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict

현물 가격을 얻으려면. 그런 다음 코드에서이 사전을 호출 할 수 있습니다. 작동하지 않습니다! 통화 사전은 불변이므로 프로그램의 수명뿐만 아니라 컴파일 되는 시점부터 동일해야합니다 ! 그래서 당신은 무엇을합니까? 한 가지 옵션은 Reader 모나드를 사용하는 것입니다.

 computePrice :: Reader CurrencyDict Dollars
 computePrice
    = do currencyDict <- ask
      --insert computation here

아마도 가장 고전적인 사용 사례는 인터프리터를 구현하는 것입니다. 하지만 그것을보기 전에 다른 기능을 소개해야합니다.

 local :: (env -> env) -> Reader env a -> Reader env a

좋습니다. 하스켈과 다른 기능적 언어는 람다 미적분을 기반으로합니다 . Lambda 미적분에는 다음과 같은 구문이 있습니다.

 data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)

이 언어에 대한 평가자를 작성하고 싶습니다. 이렇게하려면 용어와 관련된 바인딩 목록 인 환경을 추적해야합니다 (정적 범위 지정을 원하기 때문에 실제로는 클로저가됩니다).

 newtype Env = Env ([(String, Closure)])
 type Closure = (Term, Env)

완료되면 값 (또는 오류)을 얻어야합니다.

 data Value = Lam String Closure | Failure String

따라서 인터프리터를 작성해 보겠습니다.

interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
   = do env <- ask
        return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
   = do (Env env) <- ask
        case lookup (show v) env of
          -- if it is not in the environment we have a problem
          Nothing -> return . Failure $ "unbound variable: " ++ (show v)
          -- if it is in the environment, then we should interpret it
          Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
   = do v1 <- interp' t1
        case v1 of
           Failure s -> return (Failure s)
           Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!

마지막으로 사소한 환경을 전달하여 사용할 수 있습니다.

interp :: Term -> Value
interp term = runReader (interp' term) (Env [])

그리고 그게 다입니다. 람다 미적분을위한 완전한 기능의 해석기.


이에 대해 생각하는 다른 방법은 다음과 같이 질문하는 것입니다. 어떻게 구현됩니까? 대답은 독자 모나드가 실제로 모든 모나드 중에서 가장 단순하고 우아한 것 중 하나라는 것입니다.

newtype Reader env a = Reader {runReader :: env -> a}

Reader는 기능에 대한 멋진 이름입니다! 우리는 이미 정의 runReader했으므로 API의 다른 부분은 어떻습니까? 글쎄요, 모두 MonadFunctor다음과 같습니다.

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g

이제 모나드를 얻으려면 :

instance Monad (Reader env) where
   return x = Reader (\_ -> x)
   (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x

그렇게 무섭지 않습니다. ask정말 간단합니다.

ask = Reader $ \x -> x

local그렇게 나쁘지 않은 동안 :

local f (Reader g) = Reader $ \x -> runReader g (f x)

자, 리더 모나드는 함수일뿐입니다. 왜 Reader가 있습니까? 좋은 질문. 실제로 필요하지 않습니다!

instance Functor ((->) env) where
  fmap = (.)

instance Monad ((->) env) where
  return = const
  f >>= g = \x -> g (f x) x

이것들은 더 간단합니다. 무엇보다, ask그냥 idlocal전환 기능의 순서로 단지 함수의 합성입니다!


6
매우 흥미로운 대답입니다. 솔직히 모나드를 복습하고 싶을 때 여러 번 다시 읽었습니다. 그건 그렇고, nagamax 알고리즘에 대해, "values ​​<-mapM (negate. negamax (negate color)) 가능"이 정확하지 않은 것 같습니다. 여러분이 제공하는 코드는 리더 모나드가 어떻게 작동하는지 보여주기위한 것임을 압니다. 하지만 시간이 있으면 negamax 알고리즘의 코드를 수정할 수 있습니까? 왜냐하면 독자 모나드를 사용하여 negamax를 풀 때 흥미 롭기 때문입니다.
chipbk10 2013-08-13

4
그렇다면 Reader모나드 유형 클래스의 특정 구현이있는 함수입니까? 일찍 말하면 조금 덜 당황하는 데 도움이되었을 것입니다. 먼저 나는 그것을 얻지 못했습니다. 반쯤 나는 "오, 당신이 결 측값을 제공하면 원하는 결과를 얻을 수있는 무언가를 돌려 줄 수 있습니다."라고 생각했습니다. 유용하다고 생각했지만 갑자기 함수가 정확히 이것을 수행한다는 것을 깨달았습니다.
ziggystar 2014

1
이것을 읽은 후 나는 그것을 대부분 이해합니다. local기능을하지만 좀 더 설명이 필요 않습니다 ..
크리스토프 드 Troyer에게

@Philip Monad 인스턴스에 대한 질문이 있습니다. bind 함수를 다음과 같이 작성할 수 없습니까 (Reader f) >>= g = (g (f x))?
zeronone

@zeronone은 어디에 x있습니까?
Ashish Negi

56

나는 독자 모나드의 변형이 어디에나 있다는 것을 스스로 발견 할 때까지 당황했던 것을 기억 합니다. 어떻게 발견 했습니까? 작은 변형으로 판명 된 코드를 계속 작성했기 때문입니다.

예를 들어, 한때 나는 역사적 가치 를 다루는 코드를 작성하고있었습니다 . 시간이 지남에 따라 변하는 값. 이것에 대한 매우 간단한 모델은 특정 시점에서 해당 시점의 값까지의 함수입니다.

import Control.Applicative

-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }

instance Functor (History t) where
    -- Apply a function to the contents of a historical value
    fmap f hist = History (f . observe hist)

instance Applicative (History t) where
    -- A "pure" History is one that has the same value at all points in time
    pure = History . const

    -- This applies a function that changes over time to a value that also 
    -- changes, by observing both at the same point in time.
    ff <*> fx = History $ \t -> (observe ff t) (observe fx t)

instance Monad (History t) where
    return = pure
    ma >>= f = History $ \t -> observe (f (observe ma t)) t

Applicative당신이있는 경우 해당 인스턴스 수단 employees :: History Day [Person]customers :: History Day [Person]당신이 할 수 있습니다 :

-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers

즉, Functor그리고 Applicative우리가 역사와 함께 작동하도록 정기적, 비 역사적 기능을 적용 할 수 있습니다.

모나드 인스턴스는 함수를 고려하면 가장 직관적으로 이해됩니다 (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c. 유형 a -> History t b의 함수는를 값 a의 기록에 매핑하는 함수입니다 b. 예를 들어, getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP. 따라서 Monad 인스턴스 History는 다음과 같은 함수를 구성하는 것입니다. 예를 들어, getSupervisor >=> getVP :: Person -> History Day VP어떤 Person에 대해 VP그들이 가지고있는 s 의 역사 를 가져 오는 함수입니다 .

음,이 History모나드는 실제로 정확히 같은 Reader. History t aReader t a( 와) 실제로 동일 t -> a합니다.

또 다른 예 : 최근 Haskell에서 OLAP 디자인을 프로토 타이핑 했습니다. 여기서 한 가지 아이디어는 차원 집합의 교차점에서 값으로의 매핑 인 "하이퍼 큐브"입니다. 다시 시작합니다.

newtype Hypercube intersection value = Hypercube { get :: intersection -> value }

하이퍼 큐브에 대한 일반적인 작업 중 하나는 하이퍼 큐브의 해당 지점에 다중 장소 스칼라 함수를 적용하는 것입니다. 다음을위한 Applicative인스턴스를 정의하여 얻을 수 있습니다 Hypercube.

instance Functor (Hypercube intersection) where
    fmap f cube = Hypercube (f . get cube)


instance Applicative (Hypercube intersection) where
    -- A "pure" Hypercube is one that has the same value at all intersections
    pure = Hypercube . const

    -- Apply each function in the @ff@ hypercube to its corresponding point 
    -- in @fx@.
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)

History위 의 코드를 복사하여 이름을 변경했습니다. 아시다시피 HypercubeReader.

그것은 계속됩니다. 예를 들어, Reader이 모델을 적용하면 언어 통역사도로 요약됩니다 .

  • 식 = a Reader
  • 자유 변수 = 용도 ask
  • 평가 환경 = Reader실행 환경.
  • 결합 구조 = local

좋은 비유는 a 가 "구멍"이있는를 Reader r a나타내 a므로 a우리가 말하는 것을 알 수 없게합니다 . 구멍을 채우려면 aa를 제공 해야만 실제를 얻을 수 있습니다 r. 그런 것들이 아주 많습니다. 위의 예에서 "히스토리"는 시간을 지정할 때까지 계산할 수없는 값이고, 하이퍼 큐브는 교차점을 지정할 때까지 계산할 수없는 값이고, 언어 표현식은 다음을 수행 할 수있는 값입니다. 변수의 값을 제공 할 때까지 계산되지 않습니다. 또한 이러한 함수가 직관적으로 누락 된 이므로 왜 Reader r a와 같은지 에 대한 직관을 제공합니다 .r -> aar

따라서의 Functor, ApplicativeMonad인스턴스는 Reader" a가 누락 된 "종류를 모델링하는 경우에 매우 유용한 일반화이며 r이러한 "불완전한"개체를 마치 완전한 것처럼 처리 할 수 ​​있습니다.

똑같은 말을하는 또 다른 방법 : a Reader r a는를 소비 r하고 생산 a하는 것이고 Functor, ApplicativeMonad인스턴스는 Readers 작업을위한 기본 패턴입니다 . Functor= Reader다른 출력을 수정하는 a 를 만듭니다 Reader. Applicative= 두 개의 Readers를 동일한 입력에 연결 하고 출력을 결합합니다. Monad= a의 결과를 검사하고 Reader이를 사용하여 다른 Reader. localwithReader기능 = a를 Reader수정 다른 입력 것을 Reader.


5
좋은 대답입니다. GeneralizedNewtypeDeriving확장을 사용하여 Functor,Applicative , Monad자신의 기본 유형을 기반으로 newtypes를 들어, 등.
Rein Henrichs

20

Java 또는 C ++에서는 문제없이 어디서나 모든 변수에 액세스 할 수 있습니다. 코드가 다중 스레드가되면 문제가 나타납니다.

Haskell에서는 한 함수에서 다른 함수로 값을 전달하는 두 가지 방법 만 있습니다.

  • 호출 가능 함수의 입력 매개 변수 중 하나를 통해 값을 전달합니다. 단점은 다음과 같습니다. 1) 모든 변수를 그런 식으로 전달할 수는 없습니다. 입력 매개 변수 목록은 마음을 날려 버립니다. 2) 함수 호출 순서 : fn1 -> fn2 -> fn3, 함수 fn2는에서 fn1로 전달하는 매개 변수가 필요하지 않을 수 있습니다 fn3.
  • 일부 모나드의 범위에서 값을 전달합니다. 단점은 모나드 개념이 무엇인지 확실히 이해해야한다는 것입니다. 값을 전달하는 것은 Monads를 사용할 수있는 많은 응용 프로그램 중 하나 일뿐입니다. 사실 Monad의 개념은 믿을 수 없을 정도로 강력합니다. 한 번에 통찰력을 얻지 못했다면 당황하지 마십시오. 계속 시도하고 다른 튜토리얼을 읽으십시오. 당신이 얻게 될 지식은 갚을 것입니다.

Reader 모나드는 함수간에 공유하려는 데이터 만 전달합니다. 함수는 해당 데이터를 읽을 수 있지만 변경할 수는 없습니다. 그게 Reader 모나드의 전부입니다. 글쎄, 거의 다. 또한 다음과 같은 여러 기능이 있습니다.local 있지만 처음으로 asks만 사용할 수 있습니다 .


3
모나드를 사용하여 암시 적으로 데이터를 전달할 때의 또 다른 단점은 do-notation 에서 많은 ' 명령어 스타일'코드를 작성하는 것이 매우 쉽다 는 것입니다.
Benjamin Hodgson

4
@BenjaminHodgson do -notation에서 모나드를 사용하여 '명령형 모양'코드를 작성하는 것은 부 효과적인 (불순한) 코드를 작성하는 것을 의미하지 않습니다. 실제로 Haskell의 부 효과적인 코드는 IO 모나드 내에서만 가능할 수 있습니다.
Dmitry Bespalov 2014

다른 함수가 where절에 의해 하나에 첨부되면 변수를 전달하는 세 번째 방법으로 허용됩니까?
Elmex80s
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.