В Haskell используется инъекция зависимости, используя ExistentialQuantification анти-шаблон?
Я новичок Haskell, и я думаю о том, как я могу модулировать приложение Rest, которое, по существу, проходит вокруг ReaderT везде. Я разработал примитивный рабочий пример того, как это сделать (ниже) с помощью ExistentialQuantification. В комментарии к соответствующему ответу пользователь UserMotoryOrchid утверждал что-то похожее на анти-шаблон. Это анти-шаблон? В новинках вы можете объяснить, почему, если так, и показать лучшую альтернативу?
{-# 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
Ответы
Ответ 1
Когда вы сопоставляете шаблон по 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
}
Это основной аргумент против использования экзистенциальных типов в том виде, в котором вы это сделали, - есть полностью эквивалентная альтернатива, которая не требует экзистенциальных типов.
Ответ 2
Аргумент против экзистенциальных типов довольно прост (и силен): часто вы можете избежать как экзистенциального типа, так и типа класса, и вместо этого использовать простые функции.
Это явно тот случай, когда ваш класс имеет форму
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
Вышеупомянутый интерфейс является "чистым" (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
изменять базовое представление для базы данных. I.e., 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 neglect other methods for simplicity 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
просмотрел журнал, чтобы узнать, было ли последнее действие над элементом элементом вставки или удаления. Это не будет иметь большой производительности.