"Free Monad + Interpreter"패턴은 무엇입니까?


95

사람들이 Free Monad with Interpreter 에 대해 이야기하는 것을 보았습니다 . 특히 데이터 액세스와 관련하여 특히 그렇습니다. 이 패턴은 무엇입니까? 언제 사용하고 싶습니까? 어떻게 작동하며 어떻게 구현합니까?

나는 (같은 글에서 이해 이 데이터 액세스에서 모델을 분리 관하여 것을). 잘 알려진 리포지토리 패턴과 어떻게 다릅니 까? 그들은 같은 동기를 갖는 것으로 보인다.

답변:


138

실제 패턴은 실제로 데이터 액세스보다 훨씬 일반적입니다. AST를 제공하는 도메인 별 언어를 만든 다음 원하는대로 AST를 "실행"하는 하나 이상의 통역사가있는 간단한 방법입니다.

무료 모나드 부분은 많은 사용자 정의 코드를 작성하지 않고도 하스켈의 표준 모나드 기능 (예 : 표기법)을 사용하여 조립할 수있는 AST를 얻는 편리한 방법입니다. 또한 DSL을 구성 할 수 있도록합니다. DSL 을 부품으로 정의한 다음 부품을 구조화 된 방식으로 조합하여 기능과 같은 Haskell의 일반적인 추상화를 활용할 수 있습니다.

무료 모나드를 사용하면 구성 가능한 DSL 의 구조 가 제공됩니다 . 조각을 지정하기 만하면됩니다. DSL의 모든 동작을 포함하는 데이터 형식 만 작성하면됩니다. 이러한 작업은 데이터 액세스뿐만 아니라 모든 작업을 수행 할 수 있습니다. 그러나 모든 데이터 액세스를 작업으로 지정한 경우 데이터 쿼리에 대한 모든 쿼리와 명령을 지정하는 AST가 표시됩니다. 그런 다음이를 원하는대로 해석 할 수 있습니다. 라이브 데이터베이스에 대해 실행하거나 모의에 대해 실행하고 디버깅 명령을 기록하거나 쿼리를 최적화하십시오.

키 값 저장소와 같은 매우 간단한 예를 살펴 보겠습니다. 지금은 키와 값을 모두 문자열로 취급하지만 약간의 노력으로 유형을 추가 할 수 있습니다.

data DSL next = Get String (String -> next)
              | Set String String next
              | End

next매개 변수를 사용하면 작업을 결합 할 수 있습니다. 이것을 사용하여 "foo"를 받고 해당 값으로 "bar"를 설정하는 프로그램을 작성할 수 있습니다.

p1 = Get "foo" $ \ foo -> Set "bar" foo End

불행히도 이것은 의미있는 DSL에는 충분하지 않습니다. next컴포지션에 사용 했으므로 유형은 p1프로그램과 같은 길이입니다 (예 : 3 명령).

p1 :: DSL (DSL (DSL next))

이 특정 예제에서는 이와 next같이 사용 하는 것이 조금 이상해 보이지만 동작에 다른 유형 변수를 갖도록하는 것이 중요합니다. 우리는 형식화를 할 수 있습니다 getset예를 들어,.

next각 동작마다 필드가 어떻게 다른지 확인하십시오 . 이것은 DSLfunctor 를 만들기 위해 사용할 수 있음을 암시합니다 .

instance Functor DSL where
  fmap f (Get name k)          = Get name (f . k)
  fmap f (Set name value next) = Set name value (f next)
  fmap f End                   = End

실제로 이것이 Functor로 만드는 유일한 유효한 방법이므로 확장 deriving을 활성화하여 인스턴스를 자동으로 만드는 데 사용할 수 있습니다 DeriveFunctor.

다음 단계는 Free유형 자체입니다. 그것이 우리가 AST 구조 를 나타내는 데 사용 하는 DSL유형입니다. "cons"는 다음 과 같은 functor를 중첩시키는 유형 레벨 의 목록처럼 생각할 수 있습니다 DSL.

-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a   = Cons a (List a)     | Nil

따라서 Free DSL next크기가 다른 프로그램에 동일한 유형을 제공 하는 데 사용할 수 있습니다.

p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

훨씬 더 좋은 유형이 있습니다.

p2 :: Free DSL a

그러나 모든 생성자를 가진 실제 표현은 여전히 ​​사용하기가 매우 어색합니다! 이것은 모나드 부분이 들어온 곳입니다. "free monad"라는 이름에서 알 수 있듯이 Free모나드 f(이 경우 DSL)가 functor 인 한 모나드입니다 .

instance Functor f => Monad (Free f) where
  return         = Return
  Free a >>= f   = Free (fmap (>>= f) a)
  Return a >>= f = f a

이제 우리는 어딘가로 가고 있습니다. do표기법을 사용 하여 DSL 표현을 더 좋게 만들 수 있습니다 . 유일한 질문은 무엇을 넣을 next것인가 입니다 . 글쎄, 아이디어는 Free구성을 위해 구조 를 사용하는 것이므로, 우리는 Return각 다음 필드를 놓고 do-notation이 모든 배관을 수행하게합니다.

p3 = do foo <- Free (Get "foo" Return)
        Free (Set "bar" foo (Return ()))
        Free End

이것은 더 좋지만 여전히 조금 어색합니다. 우리는이 FreeReturn여기 저기. 우리는 "리프트"으로 DSL 액션 길 : 다행히도, 우리가 이용할 수있는 패턴 거기에 Free항상-같은 우리가 그것을 포장입니다 Free및 적용 Return에 대한이 next:

liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)

이제 이것을 사용하여 각 명령의 멋진 버전을 작성하고 전체 DSL을 가질 수 있습니다.

get key       = liftFree (Get key id)
set key value = liftFree (Set key value ())
end           = liftFree End

이를 사용하여 프로그램을 작성하는 방법은 다음과 같습니다.

p4 :: Free DSL a
p4 = do foo <- get "foo"
        set "bar" foo
        end

깔끔한 요령은 p4작은 명령형 프로그램처럼 보이지만 실제로는 가치가있는 표현이라는 것입니다

Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

따라서 패턴의 무료 모나드 부분은 멋진 구문으로 구문 트리를 생성하는 DSL을 얻었습니다. End;를 사용하지 않고 구성 가능한 하위 트리를 작성할 수도 있습니다 . 예를 들어 follow키를 가져 와서 값을 얻은 다음 키 자체로 사용할 수 있습니다.

follow :: String -> Free DSL String
follow key = do key' <- get key
                get key'

지금은 follow단지 같은 우리의 프로그램에서 사용할 수 있습니다 get또는 set:

p5 = do foo <- follow "foo"
        set "bar" foo
        end

그래서 우리는 DSL에 대한 훌륭한 구성과 추상화를 얻습니다.

이제 나무가 생겼으니 패턴의 후반부 인 통역사를 보게됩니다. 패턴 매칭만으로 트리를 해석 할 수 있습니다. 이를 통해 실제 데이터 저장소에 대한 코드와 IO다른 것들을 작성할 수 있습니다 . 다음은 가상 데이터 저장소에 대한 예입니다.

runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
  do res <- getKey key
     runIO $ k res
runIO (Free (Set key value next)) =
  do setKey key value
     runIO next
runIO (Free End) = close
runIO (Return _) = return ()

이것으로 DSL끝나지 않은 조각조차도 행복하게 평가합니다 end. 행복하게도 end입력 유형 서명을로 설정하여 닫은 프로그램 만 허용하는 "안전한"버전의 함수를 만들 수 있습니다 (forall a. Free DSL a) -> IO (). 이전 서명이를 받아들이는 동안 Free DSL a을 위해 어떤 a (같은 Free DSL String, Free DSL Int등),이 버전 만 받아 Free DSL a작동하는 모든 가능한 a우리는 단지로 만들 수 있습니다 - 어떤을 end. 이렇게하면 연결이 완료된 것을 잊지 않아도됩니다.

safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO

( runIO이 유형은 재귀 호출에 제대로 작동하지 않기 때문에 시작할 수는 없습니다. 그러나 정의를 블록 runIO으로 옮기고 두 버전의 함수를 노출시키지 않고도 동일한 효과를 얻을 수 있습니다.)wheresafeRunIO

코드를 실행 IO하는 것이 우리가 할 수있는 유일한 것은 아닙니다. 테스트를 위해 State Map대신 순수하게 실행하려고 할 수 있습니다 . 이 코드를 작성하는 것이 좋습니다.

이것이 무료 모나드 + 인터프리터 패턴입니다. 무료 모나드 구조를 활용하여 모든 배관 작업을 수행하는 DSL을 만듭니다. DSL에서 do-notation과 표준 모나드 기능을 사용할 수 있습니다. 그런 다음 실제로 사용하려면 어떻게 든 해석해야합니다. 트리는 궁극적으로 데이터 구조 일 뿐이므로 다른 목적으로 원하는대로 해석 할 수 있습니다.

이를 사용하여 외부 데이터 저장소에 대한 액세스를 관리 할 때 실제로 저장소 패턴과 유사합니다. 데이터 저장소와 코드 사이를 중간에두고 둘을 분리합니다. 그러나 어떤 방식 으로든 더 구체적입니다. "리포지토리"는 항상 명시적인 AST를 가진 DSL이므로 원하는대로 사용할 수 있습니다.

그러나 패턴 자체가 그보다 더 일반적입니다. 외부 데이터베이스 나 스토리지가 반드시 필요한 것은 아닙니다. DSL에 대한 효과 또는 여러 대상을 세밀하게 제어하려는 경우 어디에서나 적합합니다.


6
왜 '무료'모나드라고 불리는가?
Benjamin Hodgson 2016 년

14
"무료"라는 이름은 ncatlab.org/nlab/show/free+object 범주 이론에서 유래 한 것이지만 "최소"모나드임을 의미합니다. 잊어 버렸습니다 "다른 구조입니다.
보이드 스티븐 스미스 주니어

3
@ BenjaminHodgson : Boyd가 완전히 맞습니다. 당신이 궁금하지 않으면 너무 걱정하지 않을 것입니다. Dan Piponi는 BayHac 에서 "무료"의 의미에 대해 큰 이야기 를했습니다. 비디오의 비주얼은 완전히 쓸모가 없으므로 슬라이드 와 함께 따르십시오 .
Tikhon Jelvis 2016 년

3
nitpick : "무료 모나드 부분은 바로 [내 강조] 사용자 지정 코드를 많이 작성하지 않고도 (같이 할-표기)는 하스켈의 표준 모나드 기능을 사용하여 어셈블 할 수있는 AST를 얻을 수있는 편리한 방법입니다." 그것은 "단지"그 이상입니다. 자유 모나드는 또한 정규화 된 프로그램 표현으로 인터프리터가- do표기는 다르지만 실제로는 "동일한" 프로그램을 구별 할 수 없습니다 .
sacundim

5
@ saacundim : 귀하의 의견에 대해 자세히 설명해 주시겠습니까? 특히 'Free monads'는 통역사가 do-notation이 다르지만 실제로 "동일한"프로그램을 구별 할 수없는 정규화 된 프로그램 표현입니다.
Giorgio

15

자유 모나드는 기본적으로 더 복잡한 것을하지 않고 계산과 같은 "모양"으로 데이터 구조를 구축하는 모나드입니다. ( 이 찾을 수 예. 온라인 * 내가 저장소 패턴 완전히 익숙하지 않다.이 데이터 구조는 다음을 소비하고 작업을 수행하는 코드 조각에 전달),하지만에서 내가 읽은 이 나타납니다 더 높은 수준의 아키텍처가 되려면 무료 모나드 + 인터프리터를 사용하여 구현할 수 있습니다. 다른 한편으로, 무료 모나드 + 인터프리터는 파서와 같은 완전히 다른 것을 구현하는 데 사용될 수도 있습니다.

*이 패턴은 모나드에만 적용되는 것이 아니며 실제로 무료 응용 프로그램이나 무료 화살표를 사용하여보다 효율적인 코드를 생성 할 수 있습니다 . ( 파서가 이것의 또 다른 예입니다. )


죄송합니다. 리포지토리에 대해 더 명확해야했습니다. 리포지토리는 기본적으로 데이터 액세스를 캡슐화하고 도메인 개체를 재수 화합니다. Dependency Inversion과 함께 사용되는 경우가 많으므로 다양한 Repo 구현을 '플러그인'할 수 있습니다 (테스트에 유용하거나 데이터베이스 또는 ORM을 전환해야하는 경우). 도메인 코드 는 도메인 객체를 어디 에서 가져 오는지repository.Get() 알지 못하고 호출 합니다.
Benjamin Hodgson 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.