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

Инъекция зависимостей в 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

Но я не знаю, каков ли этот способ управлять этой задачей.

4b9b3361

Ответ 1

Я думаю, что правильный ответ здесь, и я, вероятно, получу несколько downvotes только для того, чтобы сказать это: забыть термин зависимость. Просто забудь это. Это модное модное слово из мира ОО, но не более того.

Пусть решается реальная проблема. Имейте в виду, что вы решаете проблему, и эта проблема представляет собой конкретную задачу программирования. Не делайте свою проблему "внедрением инъекции зависимостей".

Мы возьмем пример регистратора, потому что это будет основная функциональность, которую многие программы захотят иметь, и есть много разных типов журналов: один, который регистрируется в stderr, тот, который входит в файл, базу данных, и тот, который просто ничего не делает. Чтобы объединить все их, вы хотите тип:

type Logger m = String -> m ()

Вы также можете выбрать тип fancier, чтобы сэкономить несколько нажатий клавиш:

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."

Посмотрите, как это легко? В какой-то момент это заставляет вас понять, насколько проще будет ваша жизнь, когда вы просто забудете всю бессмыслицу, которую научил вас ОО.

Ответ 2

Используйте 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, просто прочитайте учебное пособие в Control.Proxy.Tutorial.

Ответ 3

Чтобы построить ответ на ertes, я думаю, что желаемая сигнатура для printFile - это printFile :: (MonadIO m, MonadLogger m) => FilePath -> m (), которую я читаю как "Я буду печатать данный файл. Для этого мне нужно выполнить некоторые операции ввода-вывода и некоторые записи".

Я не эксперт, но вот моя попытка этого решения. Я буду благодарен за замечания и предложения о том, как улучшить это.

{-# 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 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"

Ответ 4

Другой вариант - использовать экзистенциально квантифицированные типы данных. Возьмем XMonad в качестве примера. Существует интерфейс (frobby) для макетов - 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

    ...

и экзистенциальный тип данных Layout:

-- | 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.Layout и в xmonad-contrib. И, конечно же, можно динамически переключаться между различными макетами:

-- | Set the layout of the currently viewed workspace
setLayout :: Layout Window -> X ()
setLayout l = do
    [email protected](W.StackSet { W.current = [email protected](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 } } }

Ответ 5

Включение зависимостей или разрешение зависимостей - это способ принятия решения о реализации в качестве аргумента функции. Я вставляю зависимости в большинстве дней недели на свое задание на С#.

Именованные реализации

Шаблон стратегии может быть реализован с использованием именованных реализаций следующим образом.

Перечислите имена реализаций:

data Language = French | Icelandic
    deriving (Read)

определить функцию с различными реализациями для разных имен:

newtype Greeting = Greeting String
translateGreetingTo French = Greeting "Bonjour, Monde!"
translateGreetingTo Icelandic = Greeting "Halló heimur!"

если у вас есть пользователь

type User = User { _language :: Language }

вы можете приветствовать ее, используя

greet (User (Language language)) =
   let (Greeting greeting) = (translateGreetingTo language)
      in (printStrLn greeting)

Таким образом, greet будет автоматически поддерживать новый Language при реализации translateGreetingTo. *

Реализация по умолчанию

При программировании зависимостям обычно разрешается принимать стандартные реализации. Во время тестирования большинство зависимостей должно быть заменено на простые реализации заглушек.

Чтобы добиться простых настроек по умолчанию для программирования, сохраняя при этом гибкий выбор для тестирования, определите реализацию по умолчанию, передав разумные зависимости по умолчанию:

defaultGreeting = translateGreetingTo Icelandic

Повторите для каждой функции с зависимостями (так же, как и в StructureMap, зарегистрируйте каждый интерфейс, который вы определяете с помощью реализации):

utter :: Greeting -> IO ()
utter (Greeting greeting) = printStrLn greeting
defaultUtter = utter defaultGreeting

Если теперь мы создадим более гибкую версию приветствия в зависимости от utter (вместо hardcoding printStrLn):

flexibleGreet :: (Greeting -> IO ()) -> User -> IO ()
flexibleGreet utterer (User (Language language)) = utterer (translateGreetingToLanguage language)

Тогда снова мы можем refator greet как:

greet = flexibleGreet utter

Если вы пишете больше тестов, чем программный код, вы можете сохранить краткое имя гибких (зависимых) функций. И наоборот, если вы пишете больше программных кодов, чем тестов, вы можете захотеть сохранить имена версий по умолчанию короткими. Обязательно используйте систематические имена, где это возможно.


* В С# шаблон стратегии аналогичным образом может быть реализован с использованием именованной реализации:

interface IGreeting { public string Text; }
[... dependency registration and definition of User ...]
public class A { public void Greet(User user) {
  var greeting = serviceLocator.GetNamedInstance<IGreeting>(user.Language);
  WriteLine(greeting.Text);
}}