programing

Haskell의 의존성 주입 : 관용적으로 작업 해결

copyandpastes 2021. 1. 17. 12:33
반응형

Haskell의 의존성 주입 : 관용적으로 작업 해결


의존성 주입을위한 관용적 Haskell 솔루션은 무엇입니까?

예를 들어 인터페이스 가 있고 주위 frobby를 준수하는 인스턴스를 전달 해야한다고 가정합니다 (예 :, ) frobby같은 여러 종류의 인스턴스가있을 수 있습니다 .foobar

일반적인 작업은 다음과 같습니다.

  • 일부 값을 취하고 일부 값 X을 반환 하는 함수 Y. 예를 들어 이것은 SQL 쿼리 및 커넥터를 가져와 데이터 세트를 반환하는 데이터베이스 접근 자일 수 있습니다. postgres, mysql 및 모의 테스트 시스템을 구현해야 할 수도 있습니다.

  • 런타임에 선택된 특정 또는 스타일에 특화된에 Z관련된 클로저를 반환하는 일부 값을 취하는 함수 .Zfoobar

한 사람이 다음과 같이 문제를 해결했습니다.

http://mikehadlow.blogspot.com/2011/05/dependency-injection-haskell-style.html

하지만 이것이이 작업을 관리하는 표준 방법인지는 모르겠습니다.


나는 여기에 적절한 대답이 있다고 생각하며, 아마도 이것을 말하는 것에 대해 몇 가지 반대표를 받게 될 것입니다 . 의존성 주입 이라는 용어를 잊어 버리십시오 . 그냥 잊어 버려. OO 세계에서 유행하는 유행어이지만 그 이상은 아닙니다.

진짜 문제를 해결합시다. 당신이 문제를 해결하고 있다는 것을 명심하세요. 그리고 그 문제는 당면한 특정한 프로그래밍 작업입니다. 문제를 "의존성 주입 구현"으로 만들지 마십시오.

로거의 예를 살펴 보겠습니다. 로거는 많은 프로그램이 갖고 싶어하는 기본적인 기능이기 때문에 다양한 유형의 로거가 있습니다. 하나는 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."

이것이 얼마나 쉬운 지 보십니까? 어떤 시점에서 이것은 OO가 가르쳐 준 모든 말도 안되는 것을 잊었을 때 당신의 삶이 얼마나 쉬울 것인지 깨닫게합니다.


사용 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 스트리밍 또는 대화 형 동작을 모듈 식 구성 요소로 분할하여 원하는대로 혼합하고 일치시킬 수 있습니다. 이것이 종속성 주입의 핵심입니다.

에 대해 자세히 알아 보려면 Control.Proxy.Tutorialpipes 에서 자습서를 읽으십시오 .


ertes의 대답을 기반으로하기 위해 원하는 서명 printFileprintFile :: (MonadIO m, MonadLogger m) => FilePath -> m ()"I will print the given file.이를 위해 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를 예로 들어. frobby레이아웃을위한 ( ) 인터페이스가 있습니다 – LayoutClasstypeclass :

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

    ...

and existential data type 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)

that can wrap any (foo or bar) instance of LayoutClass interface. It is itself a layout:

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

Now it is possible to use Layout data type generically with only LayoutClass interface methods. Appropriate layout which implements LayoutClass interface will be selected at run-time, there is a bunch of them in XMonad.Layout and in xmonad-contrib. And, of course, it is possible to switch between different layouts dynamically:

-- | 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 } } }

ReferenceURL : https://stackoverflow.com/questions/14327327/dependency-injection-in-haskell-solving-the-task-idiomatically

반응형