11

で、main構成ファイルを読み取って、問題なく提供できますrunReader (somefunc) myEnv。ただし、リーダーの供給にsomefuncアクセスする必要はなく、チェーンの次のカップルも必要ありません。myEnvmyEnv から何かを必要とする関数は、小さなリーフ関数です。

介在するすべての関数を としてタグ付けせずに、関数内の環境にアクセスするにはどうすればよい(Reader Env)ですか? そうしないと、そもそも myEnv を渡すだけなので、それは正しくありません。また、未使用のパラメーターを複数レベルの関数に渡すのは、見苦しいものです (そうではありませんか?)。

私がネット上で見つけることができる例はたくさんありますが、それらはすべて、runReader と環境へのアクセスの間に 1 つのレベルしかないようです。


Chris Taylor のものを受け入れるのは、それが最も完全であり、他の人にとって有用であることがわかるからです。私の質問に実際に直接答えようとした唯一の人物である Heatsink にも感謝します。

問題のテスト アプリについては、Reader を完全に捨てて、環境を渡します。それは私に何も買わない。

関数 h に静的データを提供すると、その型シグネチャだけでなく、それを呼び出す g と g を呼び出す f の型シグネチャも変更されるという考えに、私はまだ困惑していると言わざるを得ません。関係する実際の型と計算は変更されていませんが、これはすべてです。実装の詳細がコード全体に漏れているように見えますが、実際には何のメリットもありません。

4

5 に答える 5

8

これはあなたが思っているほど悪くはありませんが、すべてに の戻り値の型を与えます。Reader Env aすべてにこのタグが必要な理由はf、環境に依存する場合:

type Env = Int

f :: Int -> Reader Int Int
f x = do
  env <- ask
  return (x + env)

そしてg呼び出しますf

g x = do
  y <- f x
  return (x + y)

次にg、環境にも依存します-行にバインドされた値y <- f xは、渡される環境に応じて異なる場合があるため、適切なタイプは次のとおりgです

g :: Int -> Reader Int Int

これは実際には良いことです!型システムにより、関数がグローバル環境に依存する場所を明示的に認識する必要があります。フレーズのショートカットを定義することで、タイピングの手間を省くことができますReader Int

type Global = Reader Int

型注釈は次のようになります。

f, g :: Int -> Global Int

これはもう少し読みやすいです。


これに代わる方法は、環境をすべての関数に明示的に渡すことです。

f :: Env -> Int -> Int
f env x = x + env

g :: Env -> Int -> Int
g x = x + (f env x)

Readerこれは機能し、実際、構文的にはモナドを使用するよりも悪くありません。セマンティクスを拡張したい場合に問題が発生します。Int関数の適用をカウントするタイプの更新可能な状態を持つことにも依存しているとします。次に、関数を次のように変更する必要があります。

type Counter = Int

f :: Env -> Counter -> Int -> (Int, Counter)
f env counter x = (x + env, counter + 1)

g :: Env -> Counter -> Int -> (Int, Counter)
g env counter x = let (y, newcounter) = f env counter x
                  in (x + y, newcounter + 1)

これは明らかに快適ではありません。一方、モナドのアプローチを取っている場合は、単純に再定義します

type Global = ReaderT Env (State Counter)

との古い定義は、問題なく機能fし続けます。gそれらをアプリケーション カウント セマンティクスを持つように更新するには、単純に次のように変更します。

f :: Int -> Global Int
f x = do
  modify (+1)
  env <- ask
  return (x + env)

g :: Int -> Global Int
g x = do
  modify(+1)
  y <- f x
  return (x + y)

そして今では完全に機能します。2 つの方法を比較します。

  • 環境と状態を明示的に渡すには、プログラムに新しい機能を追加したいときに完全な書き直しが必要でした。

  • モナド インターフェイスを使用するには、3 行の変更が必要でした。最初の行を変更した後もプログラムは動作し続けました。つまり、リファクタリングを段階的に行うことができ (そして変更のたびにテストすることができました)、リファクタリングが導入される可能性を減らしました。新しいバグ。

于 2012-06-27T14:45:49.863 に答える
5

いいえ。介在するすべての関数を完全に としてタグ付けするかReader Env少なくともEnv環境のあるモナドで実行されているようにタグ付けします。そして、それは完全にどこにでも渡されます。これは完全に正常な動作です。ただし、思ったほど非効率ではありません。コンパイラは、多くの場合、そのようなものを最適化して取り除きます。

基本的に、Readerモナドを使用するものはすべて、それが非常に下にあるものであっても、それ自身であるべきReaderです。(何かがモナドを使用せず、Readerモナドを使用する他のものを呼び出さない場合、それは である必要はありませんReader。)

つまり、モナドを使用するということは、環境を明示的Readerに渡す必要がないことを意味します。モナドによって自動的に処理されます。

(環境自体ではなく、渡される環境への単なるポインタであることを忘れないでください。そのため、非常に安価です。)

于 2012-06-27T13:19:16.813 に答える
3

これらは、で1回だけ初期化されるため、真にグローバル変数ですmain。この状況では、グローバル変数を使用するのが適切です。unsafePerformIOIOが必要な場合は、を使用してそれらを書き込む必要があります。

構成ファイルのみを読んでいる場合、それは非常に簡単です。

config :: Config
{-# NOINLINE config #-}
config = unsafePerformIO readConfigurationFile

他のコードに依存しているため、構成ファイルをいつロードするかを制御する必要がある場合は、より複雑になります。

globalConfig :: MVar Config
{-# NOINLINE globalConfig #-}
globalConfig = unsafePerformIO newEmptyMVar

-- Call this from 'main'
initializeGlobalConfig :: Config -> IO ()
initializeGlobalConfig x = putMVar globalConfig x

config :: Config
config = unsafePerformIO $ do
  when (isEmptyMVar globalConfig) $ fail "Configuration has not been loaded"
  readMVar globalConfig

参照:

  1. Haskellでグローバルフラグを処理する適切な方法
  2. HaskellのunsafePerformIOを介したグローバル変数
于 2012-06-27T14:02:43.503 に答える
3

便利なもう 1 つの手法は、構成ファイルの値を部分的に適用して、リーフ関数自体を渡すことです。もちろん、これは、リーフ関数を置き換えることができることが何らかの形で有利な場合にのみ意味があります。

于 2012-06-27T14:17:28.760 に答える
0

小さなリーフ関数まですべてをモナドに入れたくない場合、データを使用して、最上位Readerのモナドから必要な項目を抽出し、それらを通常のパラメーターとして渡すことができますか?Readerリーフ関数まで?これにより、間にあるすべてのものを にする必要がなくなりますがReader、リーフ関数が の機能Readerを使用するために内部にあることを知る必要がある場合は、インスタンスReader内で実行する必要から逃れることはできません。Reader

于 2012-06-27T13:54:08.120 に答える