56

依存性注入のための慣用的な Haskell ソリューションは何ですか?

たとえば、 interface がfrobbyあり、 around に準拠するインスタンスを渡す必要があるとします(これらのインスタンスには、 、 、 などfrobby、複数の種類がある可能性があります)。foobar

典型的な操作は次のとおりです。

  • 何らかの値を取り、何らかの値Xを返す関数Y。たとえば、これはデータベース アクセサーであり、SQL クエリとコネクタを受け取り、データセットを返します。postgres、mysql、および模擬テスト システムを実装する必要がある場合があります。

  • 何らかの値を取り、実行時に選択された、特定のまたはスタイルに特化した にZ関連するクロージャーを返す関数。Zfoobar

ある人は次のように問題を解決しました。

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

しかし、それがこのタスクを管理する標準的な方法であるかどうかはわかりません。

4

4 に答える 4

129

ここでの適切な答えは次のとおりだと思います。これを言うだけで、おそらくいくつかの反対票を受け取るでしょう。依存性注入という用語を忘れてください。それを忘れて。これは 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."

これがどれほど簡単か分かりますか?ある時点で、オブジェクト指向が教えてくれたナンセンスをすべて忘れれば、人生がどれほど楽になるかを実感できます。

于 2013-01-15T00:53:55.973 に答える
15

を使用し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

次に、データベースへの 1 つのインターフェイスをモデル化できます。

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のチュートリアルを参照してください。

于 2013-01-14T23:42:14.867 に答える
4

もう1つのオプションは、存在記号データ型を使用することです。例として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

    ...

および実存的なデータ型レイアウト

-- | 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を実装する適切なレイアウトは実行時に選択されます。XMonad.Layoutxmonad-contribに多数あります。そしてもちろん、異なるレイアウトを動的に切り替えることも可能です。LayoutClass

-- | 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 } } }
于 2013-01-15T17:16:25.127 に答える