Подтвердить что ты не робот

В 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
4b9b3361

Ответ 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 просмотрел журнал, чтобы узнать, было ли последнее действие над элементом элементом вставки или удаления. Это не будет иметь большой производительности.