리더 모나드는 너무 복잡하고 쓸모없는 것 같습니다. Java 또는 C ++와 같은 명령형 언어에서는 독자 모나드에 해당하는 개념이 없습니다.
간단한 예를 들어 보시고 조금 더 정리해 주시겠습니까?
리더 모나드는 너무 복잡하고 쓸모없는 것 같습니다. Java 또는 C ++와 같은 명령형 언어에서는 독자 모나드에 해당하는 개념이 없습니다.
간단한 예를 들어 보시고 조금 더 정리해 주시겠습니까?
답변:
겁 먹지마! 리더 모나드는 실제로 그렇게 복잡하지 않고 사용하기 쉬운 유틸리티를 가지고 있습니다.
모나드에 접근하는 방법에는 두 가지가 있습니다.
첫 번째 접근 방식에서 독자 모나드는 추상적 인 유형입니다.
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의 다른 부분은 어떻습니까? 글쎄요, 모두 Monad
는 Functor
다음과 같습니다.
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
그냥 id
및 local
전환 기능의 순서로 단지 함수의 합성입니다!
Reader
모나드 유형 클래스의 특정 구현이있는 함수입니까? 일찍 말하면 조금 덜 당황하는 데 도움이되었을 것입니다. 먼저 나는 그것을 얻지 못했습니다. 반쯤 나는 "오, 당신이 결 측값을 제공하면 원하는 결과를 얻을 수있는 무언가를 돌려 줄 수 있습니다."라고 생각했습니다. 유용하다고 생각했지만 갑자기 함수가 정확히 이것을 수행한다는 것을 깨달았습니다.
local
기능을하지만 좀 더 설명이 필요 않습니다 ..
(Reader f) >>= g = (g (f x))
?
x
있습니까?
나는 독자 모나드의 변형이 어디에나 있다는 것을 스스로 발견 할 때까지 당황했던 것을 기억 합니다. 어떻게 발견 했습니까? 작은 변형으로 판명 된 코드를 계속 작성했기 때문입니다.
예를 들어, 한때 나는 역사적 가치 를 다루는 코드를 작성하고있었습니다 . 시간이 지남에 따라 변하는 값. 이것에 대한 매우 간단한 모델은 특정 시점에서 해당 시점의 값까지의 함수입니다.
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 Supervisor
및 getVP :: Supervisor -> History Day VP
. 따라서 Monad 인스턴스 History
는 다음과 같은 함수를 구성하는 것입니다. 예를 들어, getSupervisor >=> getVP :: Person -> History Day VP
어떤 Person
에 대해 VP
그들이 가지고있는 s 의 역사 를 가져 오는 함수입니다 .
음,이 History
모나드는 실제로 정확히 같은 Reader
. History t a
은 Reader 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
위 의 코드를 복사하여 이름을 변경했습니다. 아시다시피 Hypercube
는 Reader
.
그것은 계속됩니다. 예를 들어, Reader
이 모델을 적용하면 언어 통역사도로 요약됩니다 .
Reader
ask
Reader
실행 환경.local
좋은 비유는 a 가 "구멍"이있는를 Reader r a
나타내 a
므로 a
우리가 말하는 것을 알 수 없게합니다 . 구멍을 채우려면 a
a를 제공 해야만 실제를 얻을 수 있습니다 r
. 그런 것들이 아주 많습니다. 위의 예에서 "히스토리"는 시간을 지정할 때까지 계산할 수없는 값이고, 하이퍼 큐브는 교차점을 지정할 때까지 계산할 수없는 값이고, 언어 표현식은 다음을 수행 할 수있는 값입니다. 변수의 값을 제공 할 때까지 계산되지 않습니다. 또한 이러한 함수가 직관적으로 누락 된 이므로 왜 Reader r a
와 같은지 에 대한 직관을 제공합니다 .r -> a
a
r
따라서의 Functor
, Applicative
및 Monad
인스턴스는 Reader
" a
가 누락 된 "종류를 모델링하는 경우에 매우 유용한 일반화이며 r
이러한 "불완전한"개체를 마치 완전한 것처럼 처리 할 수 있습니다.
똑같은 말을하는 또 다른 방법 : a Reader r a
는를 소비 r
하고 생산 a
하는 것이고 Functor
, Applicative
및 Monad
인스턴스는 Reader
s 작업을위한 기본 패턴입니다 . Functor
= Reader
다른 출력을 수정하는 a 를 만듭니다 Reader
. Applicative
= 두 개의 Reader
s를 동일한 입력에 연결 하고 출력을 결합합니다. Monad
= a의 결과를 검사하고 Reader
이를 사용하여 다른 Reader
. local
및 withReader
기능 = a를 Reader
수정 다른 입력 것을 Reader
.
GeneralizedNewtypeDeriving
확장을 사용하여 Functor
,Applicative
, Monad
자신의 기본 유형을 기반으로 newtypes를 들어, 등.
Java 또는 C ++에서는 문제없이 어디서나 모든 변수에 액세스 할 수 있습니다. 코드가 다중 스레드가되면 문제가 나타납니다.
Haskell에서는 한 함수에서 다른 함수로 값을 전달하는 두 가지 방법 만 있습니다.
fn1 -> fn2 -> fn3
, 함수 fn2
는에서 fn1
로 전달하는 매개 변수가 필요하지 않을 수 있습니다 fn3
.Reader 모나드는 함수간에 공유하려는 데이터 만 전달합니다. 함수는 해당 데이터를 읽을 수 있지만 변경할 수는 없습니다. 그게 Reader 모나드의 전부입니다. 글쎄, 거의 다. 또한 다음과 같은 여러 기능이 있습니다.local
있지만 처음으로 asks
만 사용할 수 있습니다 .
do
-notation 에서 많은 ' 명령어 스타일'코드를 작성하는 것이 매우 쉽다 는 것입니다.
where
절에 의해 하나에 첨부되면 변수를 전달하는 세 번째 방법으로 허용됩니까?