답변:
실제 패턴은 실제로 데이터 액세스보다 훨씬 일반적입니다. 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
같이 사용 하는 것이 조금 이상해 보이지만 동작에 다른 유형 변수를 갖도록하는 것이 중요합니다. 우리는 형식화를 할 수 있습니다 get
및 set
예를 들어,.
next
각 동작마다 필드가 어떻게 다른지 확인하십시오 . 이것은 DSL
functor 를 만들기 위해 사용할 수 있음을 암시합니다 .
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
이것은 더 좋지만 여전히 조금 어색합니다. 우리는이 Free
와 Return
여기 저기. 우리는 "리프트"으로 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
으로 옮기고 두 버전의 함수를 노출시키지 않고도 동일한 효과를 얻을 수 있습니다.)where
safeRunIO
코드를 실행 IO
하는 것이 우리가 할 수있는 유일한 것은 아닙니다. 테스트를 위해 State Map
대신 순수하게 실행하려고 할 수 있습니다 . 이 코드를 작성하는 것이 좋습니다.
이것이 무료 모나드 + 인터프리터 패턴입니다. 무료 모나드 구조를 활용하여 모든 배관 작업을 수행하는 DSL을 만듭니다. DSL에서 do-notation과 표준 모나드 기능을 사용할 수 있습니다. 그런 다음 실제로 사용하려면 어떻게 든 해석해야합니다. 트리는 궁극적으로 데이터 구조 일 뿐이므로 다른 목적으로 원하는대로 해석 할 수 있습니다.
이를 사용하여 외부 데이터 저장소에 대한 액세스를 관리 할 때 실제로 저장소 패턴과 유사합니다. 데이터 저장소와 코드 사이를 중간에두고 둘을 분리합니다. 그러나 어떤 방식 으로든 더 구체적입니다. "리포지토리"는 항상 명시적인 AST를 가진 DSL이므로 원하는대로 사용할 수 있습니다.
그러나 패턴 자체가 그보다 더 일반적입니다. 외부 데이터베이스 나 스토리지가 반드시 필요한 것은 아닙니다. DSL에 대한 효과 또는 여러 대상을 세밀하게 제어하려는 경우 어디에서나 적합합니다.
do
표기는 다르지만 실제로는 "동일한" 프로그램을 구별 할 수 없습니다 .
자유 모나드는 기본적으로 더 복잡한 것을하지 않고 계산과 같은 "모양"으로 데이터 구조를 구축하는 모나드입니다. ( 이 찾을 수 예. 온라인 * 내가 저장소 패턴 완전히 익숙하지 않다.이 데이터 구조는 다음을 소비하고 작업을 수행하는 코드 조각에 전달),하지만에서 내가 읽은 이 나타납니다 더 높은 수준의 아키텍처가 되려면 무료 모나드 + 인터프리터를 사용하여 구현할 수 있습니다. 다른 한편으로, 무료 모나드 + 인터프리터는 파서와 같은 완전히 다른 것을 구현하는 데 사용될 수도 있습니다.
*이 패턴은 모나드에만 적용되는 것이 아니며 실제로 무료 응용 프로그램이나 무료 화살표를 사용하여보다 효율적인 코드를 생성 할 수 있습니다 . ( 파서가 이것의 또 다른 예입니다. )
repository.Get()
알지 못하고 호출 합니다.