我是一个Haskell新手,我正在考虑如何模块化我的休息应用程序,它实际上到处都是ReaderT。我设计了一个基本的工作示例,说明如何使用ExistentialQuantification (如下所示)进行操作。在对相关答案的评论中,用户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发布于 2015-07-23 03:02:51
当您在Context上进行模式匹配时,您可以在db字段中得到一个永远无法精确知道的类型的值;您可以知道的就是它是一个Database实例,因此您可以使用该类的方法。但这意味着,从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
}这是反对以您所做的方式使用存在主义类型的核心论点--有一个完全等价的替代方案,它不需要存在类型。
发布于 2015-07-23 07:53:09
反对存在主义类型的论点非常简单(而且很强):通常,您可以同时避免存在类型和类型类机器,而可以使用普通函数。
显然,您的类具有以下形式:
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以上是数据库的“纯”(非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
}注意,上面我们用insert返回了一个抽象的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]这稍微好一点,但并不像普通的实例那么简单。在这种情况下,没有直接的赢家,使用哪种风格是个人喜好的问题。
就我个人而言,我尽可能地远离存在量化的类,因为在许多情况下,它们都输给了更简单的方法。然而,我并不是教条的,我允许自己使用“反模式”,当选择开始变得太笨拙。
作为另一种选择,可以使用外部函数在抽象级别工作,只有:
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只能通过其“方法”访问数据库,并且只能在闭包上构建闭包。如果我们还实现了一个remove方法,那么多次应用insert和delete会导致越来越大的内存占用,因为remove不能从底层数据结构中删除元素,而只能构建另一个跳过已删除元素的闭包。更实际地说,就像insert和remove只是简单地附加到日志中一样,search扫描了日志以查看元素上的最新操作是插入还是删除。这将不会有很好的表现。
https://stackoverflow.com/questions/31577342
复制相似问题