2017-01-19 14 views
1

私は、提示されたアイデアに従ってモナドトランスを使用して小さなDSLを書いています ここにhere。 イラストレーションのために、ここでは小さなサブセットを示します。MTLを使用したDSLの問題を分ける

class Monad m => ProjectServiceM m where 
    -- | Create a new project. 
    createProject :: Text --^Name of the project 
       -> m Project 
    -- | Fetch all the projects. 
    getProjects :: m [Project] 
    -- | Delete project. 
    deleteProject :: Project -> m() 

このDSLのアイデアは、APIレベルのテストを書くことができるようにすることです。このため、すべて これらのアクションcreateProject,getProjectsdeleteProjectは、WebサービスへのREST呼び出しによって実装された になります。

私はまた、期待を書くためにDSLを書いた。スニペットは以下の通りである:

class (MonadError e m, Monad m) => ExpectationM e m | m -> e where 
    shouldContain :: (Show a, Eq a) => [a] -> a -> m() 

そして、あなたはより多くのDSLのロギングのための混合物に添加することができることを想像し、 パフォーマンスメトリックsee the gist linked aboveことができます。

createProjectCreates :: (ProjectServiceM m, ExpectationM e m) => m() 
createProjectCreates = do 
    p <- createProject "foobar" 
    ps <- getProjects 
    ps `shouldContain` p 

二つの通訳を以下に示します:

newtype ProjectServiceREST m a = 
    ProjectServiceREST {runProjectServiceREST :: m a} 
    deriving (Functor, Applicative, Monad, MonadIO) 

type Error = Text 
instance (MonadIO m, MonadError Text m) => ProjectServiceM (ProjectServiceREST m) where 
    createProject projectName = return $ Project projectName 
    getProjects = return [] 
    deleteProject p = ProjectServiceREST (throwError "Cannot delete") 

newtype ExpectationHspec m a = 
    ExpectationHspec {runExpectationHspec :: m a} 
    deriving (Functor, Applicative, Monad, MonadIO) 

instance (MonadError Text m, MonadIO m) => ExpectationM Text (ExpectationHspec m) where 
    shouldContain xs x = if any (==x) xs 
         then ExpectationHspec $ return() 
         else ExpectationHspec $ throwError msg 
    where msg = T.pack (show xs) <> " does not contain " <> T.pack (show x) 

をモナド変圧器は することができシナリオcreateProjectCreatesを実行するために、これらのDSLで

は、次のようないくつかの簡単なテストを書くことが可能です異なる方法で積み重ねられています。私はそれが理にかなった一つの方法は次のとおりです。

runCreateProjectCreates :: IO (Either Text()) 
runCreateProjectCreates = (runExceptT 
          . runExpectationHspec 
          . runProjectServiceREST 
          ) createProjectCreates 

必要とする:

instance ProjectServiceM (ProjectServiceREST (ExpectationM (ExceptT Text IO))) 
instance ExpectationM Text (ProjectServiceREST (ExpectationM (ExceptT Text IO))) 

をこれに伴う問題は、ProjectSeviceMのインスタンスのいずれかが 約ExpectationMを知っているし、そのためのインスタンスを作成していることである、または逆に。これらの インスタンス容易StandaloneDeriving拡張子を使用して作成することができ、例えば:

deriving instance (ExpectationM Text m) => ExpectationM Text (ProjectServiceREST m) 

私はDSLのいずれかの実装にいくつかの 情報を漏洩していますので、これは、避けることができれば、それは素晴らしいことだが。上記の問題は に解決できますか?

答えて

0

モナドスタックの具体的なコンストラクタは、mtlスタイルのクラスに直接対応する必要はありません。 This article and Reddit discussionが適切です。 mtlクラスMonadState s mの汎用ダム実装はStateTですが、ReaderT (IORef s) IOの場合はMonadState、またはCPSバリアントの場合はMonadStateをインスタンス化できます。最終的には、エフェクトの処理方法を抽象的なままにしておくだけで、処理する必要があります。

newtype ProdT m a = ProdT { runProdT :: ... } 
    deriving (Functor, Applicative, Monad, MonadTrans, ...) 
newtype TestT m a = TestT { runTestT :: ... } 
    deriving (Functor, Applicative, Monad, MonadTrans, ...) 

、あなたは、あなたが必要とインスタンスを定義します。

は、あなたが2つの抽象モナド変圧器を書いた代わりとします。すべてのパススルーを記述する必要はなく、必要なものだけを直接書くことができます。

他のクラスの簡単な組み合わせであれば、タイプクラスを定義する以外は、ではなく、を定義することをお勧めします。クラス/インスタンス

class (MonadError e m, Monad m) => ExpectationM e m | m -> e where 
    shouldContain :: (Show a, Eq a) => [a] -> a -> m() 

作品の定義だけでだけでなくあなたは既には限りそれはMonadErrorを持っているように、ベースモナドを変更する能力を持って

shouldContain :: (MonadError e m, Show a, Eq a) => [a] -> a -> m() 

として。テストの実装は可能でしょう

newtype ExpectationT m e a = ExpectationT { runExpectation :: WriterT [e] m a } 

instance Monad m => MonadError (ExpectationT m e) e where 
    throwError = ExpectationT . tell 
    -- etc.. 
+0

「shouldContain」に関するヒントをありがとう。私は自分自身のmtlインスタンスを定義できることを理解していますが、手元の問題にどのように 'ProdT'と' TestT'を使うことができるかは不明です。明確にするために、モナド・トランスをスタッキングするとすぐに、あるクラスのインスタンスがスタック内に別のクラスを実装する必要があります。 –

関連する問題