(相关问题在运行时选择实例行为)
我想定义一种后端(可重用为独立api),然后提供多个实现并能够在运行时选择一个.
我的申请就像
data AppData =
AppData { ...
, backend :: Backend
}
Run Code Online (Sandbox Code Playgroud)
为简单起见,让我们的后端(概念代码)
data Backend key value =
Backend { get :: [(key, value)]
, cud :: key -> Maybe value -> () -- Create Update Delete
}
Run Code Online (Sandbox Code Playgroud)
现在定义我们Backend类型的正确/推荐方法是什么?
我认为这将是一元(当时Monad m),而且IO(当时MonadIO m),而且纯(那么我们需要改变-> ()的-> Backend key value)等等...
我怀疑,下一次尝试是monadic,IO纯粹,但可能是过度工程
data Backend m k v =
Backend { get :: MonadIO m => m [(k, v)]
, cud :: MonadIO m => k -> Maybe v -> Backend m k v
}
Run Code Online (Sandbox Code Playgroud)
这MonadIO是一个强大的限制和返回不可变版本cud是多余的(在大多数情况下?)与m.
抽象它的正确/推荐方法是什么?
谢谢!
一旦定义了Backend我的API,我的意图或多或少被用作(概念代码)
main = do
configuration <- getAppConfiguration
selectedBackend <- case preferedBackend configuration of
"mongoDB" -> MongoDBBackend.makeBackend
"sqlite" -> SqliteBackend.makeBackend
"volatile" -> PureDataMapBackend.makeBackend
...
appData <- makeAppData configuration selectedBackend
....
Run Code Online (Sandbox Code Playgroud)
如果您还需要非IO后端,那么我建议通过运行其运算的monad对数据类型进行参数化.正如@Cactus所指出的,不需要为数据类型本身添加约束.它没有任何帮助,只是让事情变得复杂.相反,这些约束将在创建各种Backends的函数处.
此外,尽管可能会改变更新函数的返回类型,但使用这样的函数将只是地狱,因为应用程序基本上需要在使用函数时涵盖所有(两个)情况.所以相反,我建议保持结果简单,只是monadic.对于纯后端,您可以在Statemonad中运行应用程序,例如:
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.State
data Backend m key value = Backend
{ beGet :: m [(key, value)]
, beCud :: key -> Maybe value -> m () -- Create Update Delete
}
pureBackend :: (MonadState [(k, v)] m, Eq k) => Backend m k v
pureBackend = Backend get pureCud
where
filterOut k = filter ((/= k) . fst)
pureCud k Nothing = modify (filterOut k)
pureCud k (Just v) = modify (((k, v) :) . filterOut k)
-- other backends ...
Run Code Online (Sandbox Code Playgroud)
这意味着AppData其他人使用的东西也Backend需要由monad进行参数化.