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
하지만 이것이이 작업을 관리하는 표준 방법인지는 모르겠습니다.
나는 여기에 적절한 대답이 있다고 생각하며, 아마도 이것을 말하는 것에 대해 몇 가지 반대표를 받게 될 것입니다 . 의존성 주입 이라는 용어를 잊어 버리십시오 . 그냥 잊어 버려. 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의 대답을 기반으로하기 위해 원하는 서명 printFile
은 printFile :: (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
레이아웃을위한 ( ) 인터페이스가 있습니다 – 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
...
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
'programing' 카테고리의 다른 글
문자열이 특정 패턴으로 끝나는 지 확인 (0) | 2021.01.17 |
---|---|
C #을 사용하여 자체 서명 된 인증서를 만드는 방법은 무엇입니까? (0) | 2021.01.17 |
인증서의 공개 키를 .pem 형식으로 저장하는 방법 (0) | 2021.01.17 |
중첩 된 ifelse 문 (0) | 2021.01.17 |
사용자의 이메일 주소를 얻기위한 Google OAuth API? (0) | 2021.01.17 |