27

プログラムで特定のタイプのプロシージャを表現するためのAPIを定義しようとしています。

newtype Procedure a = { runProcedure :: ? }

IDからレコードへのマッピングで構成される状態があります。

type ID = Int
data Record = { ... }
type ProcedureState = Map ID Record

3つの基本的な操作があります。

-- Declare the current procedure invalid and bail (similar to some definitions of fail for class Monad)
abort :: Procedure ()
-- Get a record from the shared state; abort if the record does not exist.
retrieve :: ID -> Procedure Record
-- Store (or overwrite) a record in the shared state.
store :: ID -> Record -> Procedure ()

これらの操作にはいくつかの目標があります。

  • プロシージャは、(生の呼び出しとは異なりMap.lookup)どのレコードが使用可能かについて仮定を立てることができ、それらの仮定のいずれかが間違っている場合、プロシージャは全体として失敗を返します。
  • <|>さまざまな仮定を行うプロシージャにフォールバックするために、(クラスAlternativeから)を使用して一連のプロシージャをチェーン化できます。(STMと同様orElse

これらの目標を考えると、モナドStateMaybeモナドの組み合わせが必要だと思います。

-- Which to choose?
type Procedure a = StateT ProcedureState Maybe a
type Procedure a = MaybeT (State ProcedureState) a

Maybeとの2つの順序がどのようにState異なるかを理解できません。2つの順序の動作の違いを誰かが説明できますか?

また、私の当初の考え方に問題がある場合(おそらく私は過剰に設計されています)、遠慮なく指摘してください。

結論: 3つの回答はすべて役に立ちましたが、どの順序が必要かを判断するのに役立つ一般的なアイデアが1つありました。runMaybeT/の戻りタイプをrunStateT見ると、どの組み合わせが私が探していた動作をしているのかが簡単にわかりました。(私の場合、リターンタイプが必要ですMaybe (ProcedureState, a))。

4

5 に答える 5

25

編集:私はもともとケースを逆にした。修正されました。

モナド変換子スタックの順序の違いは、実際には、スタックのレイヤーを剥がしているときにのみ問題になります。

type Procedure a = MaybeT (State ProcedureState) a

この場合、最初にMaybeTを実行します。これにより、。を返すステートフル計算が行われますMaybe a

type Procedure a = StateT ProcedureState Maybe a

これが外側のモナドです。これは、初期状態でStateTStateTを実行した後、が与えられることを意味しますMaybe (a, ProcedureState)。つまり、計算が成功した場合と成功しなかった場合があります。

したがって、どちらを選択するかは、部分的な計算をどのように処理するかによって異なります。MaybeT外側では、計算が成功したかどうかに関係なく、常に何らかの戻り状態が返されます。これは、役立つ場合と役に立たない場合があります。外側でStateTは、すべてのステートフルトランザクションが有効であることを保証します。あなたの説明から、私はおそらくStateT自分でバリアントを使用するでしょうが、どちらも機能することを期待しています。

モナド変換子の順序付けの唯一のルールは、IO(または別の非変換子モナド)が関係している場合、それはスタックの一番下でなければならないということです。通常ErrorT、必要に応じて、次に低いレベルとして使用します。

于 2011-02-22T09:05:23.223 に答える
15

他の答えを補足するために、一般的なケースでこれを理解する方法を説明したいと思います。つまり、2つのトランスフォーマーが与えられた場合、それらの2つの組み合わせのセマンティクスは何ですか?

先週の解析プロジェクトでモナド変換子を使い始めたとき、私はこの質問に多くの問題を抱えていました。私のアプローチは、私が確信が持てないときはいつでも相談する変換されたタイプのテーブルを作成することでした。これが私がそれをした方法です:

ステップ1:基本的なモナドタイプとそれに対応するトランスフォーマータイプのテーブルを作成します。

transformer           type                  base type (+ parameter order)

---------------------------------------------------------------

MaybeT   m a        m (Maybe a)            b.    Maybe b

StateT s m a        s -> m (a, s)          t b.  t -> (b, t)

ListT    m a        m [a]                  b.    [] b

ErrorT e m a        m (Either e a)         f b.  Either f b

... etc. ...

ステップ2:mタイプパラメーターの代わりに、各モナド変換子を各ベースモナドに適用します。

inner         outer         combined type

Maybe         MaybeT        Maybe (Maybe a)
Maybe         StateT        s -> Maybe (a, s)      --  <==  this !!
... etc. ...

State         MaybeT        t -> (Maybe a, t)      --  <== and this !!
State         StateT        s -> t -> ((a, s), t)
... etc. ...

(2次の組み合わせがあるので、この手順は少し面倒です...しかし、それは私にとって良い練習であり、一度だけ行う必要がありました。) ここでの重要な点は、組み合わせた型をラップせずに記述したことです。 --MaybeT、StateTなどの煩わしいラッパーはありません。ボイラープレートのないタイプを見て、考えるのはずっと簡単です。

元の質問に答えるために、このグラフは次のことを示しています。

  • MaybeT + State :: t -> (Maybe a, t)値がない可能性があるが、(おそらく変更された)状態出力が常に存在するステートフル計算

  • StateT + Maybe :: s -> Maybe (a, s)状態と値の両方が存在しない可能性がある計算

于 2012-12-05T13:34:43.497 に答える
8

State/StateTを使用してプロシージャの状態を保存するのではなくIORefIOモナドでを使用しているとしましょう。

先験的に、とモナドの組み合わせで動作させたいmzero(または)ことができる2つの方法があります。failIOMaybe

  • どちらかmzeroが計算全体を一掃するので、mzero <|> x = x; また
  • mzero現在の計算では値が返されませんが、IOタイプの効果は保持されます。

最初のプロシージャが必要なようです。そのため、1つのプロシージャによって設定された状態は、<|>sのチェーン内の次のプロシージャのために「展開」されます。

もちろん、このセマンティクスを実装することは不可能です。mzero計算を実行するまで、計算が呼び出されるかどうかはわかりませんが、実行すると、IOのような任意の効果が生じる可能性がlaunchTheMissilesあり、ロールバックできません。

Maybeそれでは、とから2つの異なるモナド変換子スタックを構築してみましょうIO

  • IOT Maybe-おっと、これは存在しません!
  • MaybeT IO

存在するもの(MaybeT IOmzeroは可能な動作を提供し、存在しないIOT Maybeものは他の動作に対応します。

幸い、あなたはを使用しています。その効果は、 ;State ProcedureStateではなくロールバックできます。IO必要なモナド変換子スタックがStateT ProcedureState Maybe1つです。

于 2011-02-22T15:10:16.320 に答える
4

両方のバージョンの「実行」関数を作成しようとすると、自分で質問に答えることができます。MTL+トランスフォーマーがインストールされていないため、自分で作成することはできません。一方は(多分a、state)もう一方は多分(a、state)を返します。

編集-混乱を招く可能性のある詳細が追加されるため、応答を切り捨てました。ジョンの答えは頭に釘を打ちます。

于 2011-02-22T09:05:57.180 に答える
1

概要:スタックの注文が異なれば、ビジネスロジックも異なります。

つまり、スタックのモナド変換子の順序が異なると、評価の順序だけでなく、プログラムの機能にも影響します。

注文の影響を示すとき、人々は通常、、、、、、などの最も単純なトランスフォーマーをReaderT使用します。順序が異なってもビジネスロジックが劇的に異なるわけではないため、影響を明確に理解することは困難です。さらに、それらの一部のサブセットは可換です。つまり、機能の違いはありません。WriterTStateTMaybeTExceptT

デモンストレーションの目的で、モナドスタックの変圧器の順序の劇的な違いを明らかにするStateTandを使用することをお勧めします。ListT

背景:StateTおよびListT

  • StateTStateモナドは、For a FewMonadsMoreで詳しく説明されています。StateT基になるのモナディック操作を使用して、もう少しパワーを与えますm。多くのモナドチュートリアルで説明されている、、、、およびを知っevalStateTていれば十分です。putgetmodifyState
  • ListTList、別名、、はモナドです( A Fistful of Monads[]で説明されています)。(パッケージ内)は、基になるモナドのすべてのモナド操作に似たものを提供します。トリッキーな部分は(に匹敵するもの)の実行です:実行の方法はたくさんあります。、、、を使用するときに気になるさまざまな結果について考えてみてください。モナドのコンテキストには、それらを調べる、つまり、折りたたむ、など、多くの潜在的な消費者がいます。ListT m alist-t[a]mListTevalStateTevalStateTrunStateTexecStateListtraverse_fold

実験:モナド変換子の順序の影響を理解する

デモンストレーション用のいくつかの機能を実行するために、を使用StateTして、そのListT上に単純な2層のモナドトランスフォーマースタックを構築します。IO

タスクの説明

ストリーム内の数値を合計する

Integerストリームはsのリストとして抽象化されるので、私たちListTが入ります。それらを合計するには、私たちが来るストリーム内の各アイテムを処理している間、合計の状態を維持する必要がありますStateT

2つのスタック

Int合計を維持するための単純な状態があります

  • ListT (StateT Int IO) a
  • StateT Int (ListT IO) a

フルプログラム

#!/usr/bin/env stack
-- stack script --resolver lts-11.14 --package list-t --package transformers

import ListT (ListT, traverse_, fromFoldable)
import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (StateT, evalStateT, get, modify)

main :: IO()
main =  putStrLn "#### Task: summing up numbers in a stream"
     >> putStrLn "####       stateful (StateT) stream (ListT) processing"
     >> putStrLn "#### StateT at the base: expected result"
     >> ltst
     >> putStrLn "#### ListT at the base: broken states"
     >> stlt



-- (ListT (StateT IO)) stack
ltst :: IO ()
ltst = evalStateT (traverse_ (\_ -> return ()) ltstOps) 10

ltstOps :: ListT (StateT Int IO) ()
ltstOps = genLTST >>= processLTST >>= printLTST

genLTST :: ListT (StateT Int IO) Int
genLTST = fromFoldable [6,7,8]

processLTST :: Int -> ListT (StateT Int IO) Int
processLTST x = do
    liftIO $ putStrLn "process iteration LTST"
    lift $ modify (+x)
    lift get

printLTST :: Int -> ListT (StateT Int IO) ()
printLTST = liftIO . print



-- (StateT (ListT IO)) stack
stlt :: IO ()
stlt = traverse_ (\_ -> return ())
     $ evalStateT (genSTLT >>= processSTLT >>= printSTLT) 10

genSTLT :: StateT Int (ListT IO) Int
genSTLT = lift $ fromFoldable [6,7,8]

processSTLT :: Int -> StateT Int (ListT IO) Int
processSTLT x = do
    liftIO $ putStrLn "process iteration STLT"
    modify (+x)
    get

printSTLT :: Int -> StateT Int (ListT IO) ()
printSTLT = liftIO . print

結果と説明

$ ./order.hs   
#### Task: summing up numbers in a stream
####       stateful (StateT) stream (ListT) processing
#### StateT at the base: expected result
process iteration LTST
16
process iteration LTST
23
process iteration LTST
31
#### ListT at the base: broken states
process iteration STLT
16
process iteration STLT
17
process iteration STLT
18

の後に評価されるため、最初のスタックListT (StateT Int IO) aは正しい結果を生成します。を評価するとき、ランタイムシステムはすでに-スタックにストリームを供給し、それらを通過するすべての操作を評価しました。ここで評価された言葉は、の影響がなくなったことを意味し、現在まで透過的です。StateTListTStateTListT[6,7,8]traverse_ListTListTStateT

2番目のスタックは寿命が短すぎるStateT Int (ListT IO) aため、正しい結果が得られません。StateT評価のすべての反復でListT、別名、、traverse_状態が作成され、評価され、消滅します。このStateTスタック構造のは、リスト/ストリームアイテム操作間で状態を維持するという目的を達成していません。

于 2018-06-18T20:18:10.483 に答える