我是一名Haskell新手,我正在思考如何模块化我的Rest应用程序,它基本上可以在任何地方传递ReaderT。我已经设计了一个原始的工作示例,说明如何使用ExistentialQuantification(下图)。在对relevant answer的评论中,用户MathematicalOrchid声称类似于反模式。 这个是一种反模式吗?用新手来说,你能解释一下为什么会这样,并展示一个更好的选择吗?
{-# LANGUAGE ExistentialQuantification #-}
import Control.Monad.Reader
import Control.Monad.Trans
import Data.List (intersect)
data Config = Config Int Bool
data User = Jane | John | Robot deriving (Show)
listUsers = [Jane, John, Robot]
class Database d where
search :: d -> String -> IO [User]
fetch :: d -> Int -> IO (Maybe User)
data LiveDb = LiveDb
instance Database LiveDb where
search d q = return $ filter ((q==) . intersect q . show) listUsers
fetch d i = return $ if i<3 then Just $ listUsers!!i else Nothing
data TestDb = TestDb
instance Database TestDb where
search _ _ = return [Robot]
fetch _ _ = return $ Just Robot
data Context = forall d. (Database d) => Context {
db :: d
, config :: Config
}
liveContext = Context { db = LiveDb, config = Config 123 True }
testContext = Context { db = TestDb, config = Config 123 True }
runApi :: String -> ReaderT Context IO String
runApi query = do
Context { db = db } <- ask
liftIO . fmap show $ search db query
main = do
let q = "Jn"
putStrLn $ "searching users for " ++ q
liveResult <- runReaderT (runApi q) liveContext
putStrLn $ "live result " ++ liveResult
testResult <- runReaderT (runApi q) testContext
putStrLn $ "test result " ++ testResult
import Control.Monad.Reader
import Control.Monad.Trans
import Data.List (intersect)
data Config = Config Int Bool
data User = Jane | John | Robot deriving (Show)
listUsers = [Jane, John, Robot]
data Database = Database {
search :: String -> IO [User]
, fetch :: Int -> IO (Maybe User)
}
liveDb :: Database
liveDb = Database search fetch where
search q = return $ filter ((q==) . intersect q . show) listUsers
fetch i = return $ if i<3 then Just $ listUsers!!i else Nothing
testDb :: Database
testDb = Database search fetch where
search _ = return [Robot]
fetch _ = return $ Just Robot
data Context = Context {
db :: Database
, config :: Config
}
liveContext = Context { db = liveDb, config = Config 123 True }
testContext = Context { db = testDb, config = Config 123 True }
runApi :: String -> ReaderT Context IO String
runApi query = do
d <- fmap db $ ask
liftIO . fmap show $ search d $ query
main = do
let q = "Jn"
putStrLn $ "searching users for " ++ q
liveResult <- runReaderT (runApi q) liveContext
putStrLn $ "live result " ++ liveResult
testResult <- runReaderT (runApi q) testContext
putStrLn $ "test result " ++ testResult
答案 0 :(得分:13)
当您在Context
上进行模式匹配时,您会在db
字段中找到一个您永远无法准确知道的类型的值;所有你被允许知道它是Database
实例,因此你可以使用该类&#39;用它的方法。但这意味着,从Context
类型的角度来看,存在性d
类型不再提供此类型的功能:
-- The "record of methods" pattern
data Database =
Database { search :: String -> IO [User]
, fetch :: Int -> IO (Maybe User)
}
liveDb :: Database
liveDb = Database search fetch
where search d q = return $ filter ((q==) . intersect q . show) listUsers
fetch d i = return $ if i<3 then Just $ listUsers!!i else Nothing
testDb :: Database
testDb = Database search fetch
where search _ _ = return [Robot]
fetch _ _ = return (Just Robot)
data Context =
Context { db :: Database
, config :: Config
}
这是反对以你已经完成的方式使用存在类型的核心论点 - 有一个完全等价的替代品,不需要存在类型。
答案 1 :(得分:5)
针对存在类型的论证非常简单(强大):通常,您可以避免存在类型和类型类机制,而是使用普通函数。
很明显,您的班级的格式为
class D a where
method1 :: a -> T1
method2 :: a -> T2
-- ...
与发布的Database
示例一样,因为它的实例可以用普通记录类型中的值替换
data D = {
method1 :: T1
, method2 :: T2
-- ...
}
这基本上是@LuisCasillas的解决方案。
但请注意,上述翻译依赖于T1,T2
类型不依赖于a
。如果不是这样怎么办?例如。如果我们有什么
class Database d where
search :: d -> String -> [User]
fetch :: d -> Int -> Maybe User
insert :: d -> User -> d
以上是纯粹的&#34; (no-IO)数据库接口,也允许通过insert
进行更新。然后一个实例可以
data LiveDb = LiveDb [User]
instance Database LiveDb where
search (LiveDb d) q = filter ((q==) . intersect q . show) d
fetch (LiveDb d) i = case drop i d of [] -> Nothing ; (x:_) -> Just x
insert (LiveDb d) u = LiveDb (u:d)
请注意,这里我们使用参数d
,与原始情况不同,它是占位符。
我们可以在这里没有课程和存在吗?
data Database =
Database { search :: String -> [User]
, fetch :: Int -> Maybe User
, insert :: User -> Database
}
请注意,上面我们在Database
中返回一个摘要insert
。此接口比存在性类更通用,因为它允许insert
更改数据库的基础表示。即,insert
可以从基于列表的表示移动到基于树的表示。这就像让insert
从存在量化的Database
到自身,而不是从具体的实例到自身。
无论如何,让我们以记录式的方式写LiveDb
:
liveDb :: Database
liveDb = Database (search' listUsers) (fetch' listUsers) (insert' listUsers)
where search' d q = filter ((q==) . intersect q . show) d
fetch' d i = case drop i d of [] -> Nothing ; (x:_) -> Just x
insert' d u = Database (search' d') (fetch' d') (insert' d')
where d' = u:d
listUsers = [Jane, John, Robot]
上面我必须将基础状态d
传递给每个函数,而insert
我必须更新这种状态。
总的来说,我发现上述内容比instance Database LiveDb
方法更复杂,这些方法不需要状态传递。当然,我们可以应用一些重构并澄清代码:
makeLiveDb :: [User] -> Database
makeLiveDb d = Database search fetch insert
where search q = filter ((q==) . intersect q . show) d
fetch i = case drop i d of [] -> Nothing ; (x:_) -> Just x
insert u = makeLiveDb (u:d)
liveDb :: Database
liveDb = makeLiveDb [Jane, John, Robot]
这有点好,但不如普通实例那么简单。在这种情况下没有直截了当的赢家,使用哪种风格是个人偏好的问题。
就个人而言,我尽可能远离存在量化的课程,因为在许多情况下,他们会失去更简单的方法。但是,我并没有对它们说教,并允许自己使用&#34;反模式&#34;当替代品开始变得太笨拙时。
作为替代方案,可以使用在抽象级别工作的外部函数:
data Database =
Database { search :: String -> [User]
-- let's neglect other methods for simplicity's sake
}
insert :: Database -> User -> Database
insert (Database s) u = Database s'
where s' str = s str ++ [ u | show u == str ] -- or something similar
这样做的好处是insert
适用于抽象Database
,无论其基础数据结构如何。缺点是,通过这种方式,insert
只能通过其&#34;方法&#34;来访问数据库,并且只能通过在闭包时构建闭包来工作。如果我们还实施了remove
方法,多次应用insert
和delete
会导致内存占用越来越大,因为remove
无法从基础数据中删除元素结构,但只能构建另一个跳过已删除元素的闭包。更实际的是,就好像insert
和remove
只是附加到日志中一样,search
扫描日志以查看元素上的最新操作是插入还是删除。这不会有很好的表现。