Инъекция зависимостей в Haskell: решение задачи идиоматически
что такое идиоматическое решение Haskell для инъекции зависимостей?
например, предположим, что у вас есть интерфейс frobby
, и вам нужно было передать экземпляр, соответствующий frobby
вокруг (может быть несколько разновидностей этих экземпляров, скажем,foo
и bar
).
типичные операции будут:
функции, которые принимают некоторое значение
X
и вернуть некоторое значениеY
. Например, это может быть доступ к базе данных, принимая SQL-запрос и соединитель и возврат набора данных. Возможно, вам потребуется реализовать postgres, mysql и макет тестовой системы.функции, которые принимают некоторое значение
Z
и вернуть закрытие, относящееся кZ
, специализированный для данногоfoo
илиbar
стиль, выбранный во время выполнения.
один человек решил проблему следующим образом:
http://mikehadlow.blogspot.com/2011/05/dependency-injection-haskell-style.html
но я не знаю, является ли это каноническим способом управления этой задачей.
4 ответа:
Я думаю, что правильный ответ здесь, и я, вероятно, получу несколько downvotes только для того, чтобы сказать Это: забудьте термин инъекции зависимостей. Просто забудь об этом. Это модное модное слово из мира ОО, но не более того.
давайте решим реальную проблему. Имейте в виду, что вы решаете проблему, и эта проблема является конкретной задачи программирования на руку. Не делайте свою проблему "реализация инъекции зависимостей".
мы возьмем пример регистратора, потому что это базовая часть функциональности, которую многие программы захотят иметь, и есть много разных типов регистраторов: один, который входит в stderr, один, который входит в файл, базу данных и тот, который просто ничего не делает. Чтобы объединить их всех вы хотите тип:
type Logger m = String -> m ()
вы также можете выбрать более причудливый тип, чтобы сохранить некоторые нажатия клавиш:
class PrettyPrint a where pretty :: a -> String type Logger m = forall a. (PrettyPrint a) => a -> m ()
теперь давайте определим несколько регистраторов, используя последний вариант:
noLogger :: (Monad m) => Logger m noLogger _ = return () stderrLogger :: (MonadIO m) => Logger m stderrLogger x = liftIO . hPutStrLn stderr $ pretty x fileLogger :: (MonadIO m) => FilePath -> Logger m fileLogger logF x = liftIO . withFile logF AppendMode $ \h -> hPutStrLn h (pretty x) acidLogger :: (MonadIO m) => AcidState MyDB -> Logger m acidLogger db x = update' db . AddLogLine $ pretty x
вы можете увидеть, как это строит график зависимостей. Элемент
acidLogger
зависит от подключения к базе данных дляMyDB
структура базы данных. Передача аргументов в функции-это самый естественный способ выражения зависимостей в программе. Ведь функция-это просто значение, которое зависит от другого значения. Это также верно и для действий. Если ваше действие зависит от регистратора, то, естественно, это функция лесорубы:printFile :: (MonadIO m) => Logger m -> FilePath -> m () printFile log fp = do log ("Printing file: " ++ fp) liftIO (readFile fp >>= putStr) log "Done printing."
видите, как это просто? В какой-то момент это заставляет вас понять, сколько легче будет ваша жизнь, когда вы просто забудете всю чепуху, которой вас научил ОО.
использовать
pipes
. Я не буду говорить, что это идиоматично, потому что библиотека все еще относительно новая, но я думаю, что это точно решает вашу проблему.например, предположим, что вы хотите обернуть интерфейс к некоторой базе данных:
import Control.Proxy -- This is just some pseudo-code. I'm being lazy here type QueryString = String type Result = String query :: QueryString -> IO Result database :: (Proxy p) => QueryString -> Server p QueryString Result IO r database = runIdentityK $ foreverK $ \queryString -> do result <- lift $ query queryString respond result
затем мы можем моделировать один интерфейс к базе данных:
user :: (Proxy p) => () -> Client p QueryString Result IO r user () = forever $ do lift $ putStrLn "Enter a query" queryString <- lift getLine result <- request queryString lift $ putStrLn $ "Result: " ++ result
вы соединяете их так:
это позволит пользователю взаимодействовать с базой данных из запроса.runProxy $ database >-> user
мы затем можно переключить базу данных с помощью макета базы данных:
mockDatabase :: (Proxy p) => QueryString -> Server p QueryString Result IO r mockDatabase = runIdentityK $ foreverK $ \query -> respond "42"
теперь мы можем очень легко переключить базу данных на макет:
runProxy $ mockDatabase >-> user
или мы можем отключить клиент базы данных. Например, если мы заметили, что определенный сеанс клиента вызвал какую-то странную ошибку, мы могли бы воспроизвести ее следующим образом:
reproduce :: (Proxy p) => () -> Client p QueryString Result IO () reproduce () = do request "SELECT * FROM WHATEVER" request "CREATE TABLE BUGGED" request "I DON'T REALLY KNOW SQL"
... затем подключите его так:
runProxy $ database >-> reproduce
pipes
позволяет разделить потоковое или Интерактивное поведение на модульные компоненты, чтобы вы могли смешивать и сопоставлять их, как вам угодно, что является сутью инъекции зависимости.подробнее о
pipes
, просто прочитайте учебник по адресу управление.Полномочие.Учебник.
построить на ответ эртес, я думаю, что нужные подписи для
printFile
иprintFile :: (MonadIO m, MonadLogger m) => FilePath -> m ()
, который я читаю как "я буду печатать данный файл. Для этого мне нужно сделать некоторые IO и некоторые журналы."Я не эксперт, но вот моя попытка этого решения. Я буду признателен за комментарии и предложения о том, как это улучшить.
{-# LANGUAGE FlexibleInstances #-} module DependencyInjection where import Prelude hiding (log) import Control.Monad.IO.Class import Control.Monad.Identity import System.IO import Control.Monad.State -- |Any function that can turn a string into an action is considered a Logger. type Logger m = String -> m () -- |Logger that does nothing, for testing. noLogger :: (Monad m) => Logger m noLogger _ = return () -- |Logger that prints to STDERR. stderrLogger :: (MonadIO m) => Logger m stderrLogger x = liftIO $ hPutStrLn stderr x -- |Logger that appends messages to a given file. fileLogger :: (MonadIO m) => FilePath -> Logger m fileLogger filePath value = liftIO logToFile where logToFile :: IO () logToFile = withFile filePath AppendMode $ flip hPutStrLn value -- |Programs have to provide a way to the get the logger to use. class (Monad m) => MonadLogger m where getLogger :: m (Logger m) -- |Logs a given string using the logger obtained from the environment. log :: (MonadLogger m) => String -> m () log value = do logger <- getLogger logger value -- |Example function that we want to run in different contexts, like -- skip logging during testing. printFile :: (MonadIO m, MonadLogger m) => FilePath -> m () printFile fp = do log ("Printing file: " ++ fp) liftIO (readFile fp >>= putStr) log "Done printing." -- |Let's say this is the real program: it keeps the log file name using StateT. type RealProgram = StateT String IO -- |To get the logger, build the right fileLogger. instance MonadLogger RealProgram where getLogger = do filePath <- get return $ fileLogger filePath -- |And this is how you run printFile "for real". realMain :: IO () realMain = evalStateT (printFile "file-to-print.txt") "log.out" -- |This is a fake program for testing: it will not do any logging. type FakeProgramForTesting = IO -- |Use noLogger. instance MonadLogger FakeProgramForTesting where getLogger = return noLogger -- |The program doesn't do any logging, but still does IO. fakeMain :: IO () fakeMain = printFile "file-to-print.txt"
другой вариант-использовать экзистенциально количественно типов данных. Давайте возьмем XMonad в качестве примера. Существует () для макетов –
LayoutClass
typeclass:-- | Every layout must be an instance of 'LayoutClass', which defines -- the basic layout operations along with a sensible default for each. -- -- ... -- class Show (layout a) => LayoutClass layout a where ...
и экзистенциальный тип данных планировка:
-- | An existential type that can hold any object that is in 'Read' -- and 'LayoutClass'. data Layout a = forall l. (LayoutClass l a, Read (l a)) => Layout (l a)
который может обернуть любой (
foo
илиbar
) экземплярLayoutClass
интерфейс. Это сам макет:instance LayoutClass Layout Window where runLayout (Workspace i (Layout l) ms) r = fmap (fmap Layout) `fmap` runLayout (Workspace i l ms) r doLayout (Layout l) r s = fmap (fmap Layout) `fmap` doLayout l r s emptyLayout (Layout l) r = fmap (fmap Layout) `fmap` emptyLayout l r handleMessage (Layout l) = fmap (fmap Layout) . handleMessage l description (Layout l) = description l
теперь можно использовать
Layout
тип данных в общем случае только сLayoutClass
методы интерфейса. Соответствующий макет, который реализуетLayoutClass
интерфейс будет выбран во время выполнения, есть куча их в XMonad.Макет и xmonad-contrib. И, конечно же, можно динамически переключаться между различными макетами:-- | Set the layout of the currently viewed workspace setLayout :: Layout Window -> X () setLayout l = do ss@(W.StackSet { W.current = c@(W.Screen { W.workspace = ws })}) <- gets windowset handleMessage (W.layout ws) (SomeMessage ReleaseResources) windows $ const $ ss {W.current = c { W.workspace = ws { W.layout = l } } }