在Haskell中使用ExistentialQuantification进行依赖注入反模式?

时间:2015-07-23 02:46:10

标签: haskell dependency-injection

我是一名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

2 个答案:

答案 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方法,多次应用insertdelete会导致内存占用越来越大,因为remove无法从基础数据中删除元素结构,但只能构建另一个跳过已删除元素的闭包。更实际的是,就好像insertremove只是附加到日志中一样,search扫描日志以查看元素上的最新操作是插入还是删除。这不会有很好的表现。