10

私は Haskell で小さなゲームを書こうとしていますが、渡すためにかなりの量の状態が必要です。Stateモナドで状態を隠してみたい

今、私は問題に遭遇しました: 状態と引数を取る関数は、状態モナドで動作するように書くのは簡単でした. しかし、引数として状態を受け取るだけの関数もあります (そして、変更された状態、またはおそらく何か他のものを返します)。

コードの一部に、次の行があります。

let player = getCurrentPlayer state

状態を取らず、代わりに書きたい

player <- getCurrentPlayerM

現在、その実装は次のようになっています

getCurrentPlayer gameState = 
  (players gameState) ! (on_turn gameState)

そして、次のように書くことで State モナドで動作させるのに十分簡単に​​思えました:

getCurrentPlayerM = do state <- get
                       return (players state ! on_turn state)

しかし、それはghcからの苦情を引き起こします! 「get」の使用に起因する (MonadState GameState m0) のインスタンスはありません。State モナド形式で nullary でないことを除いて、非常によく似た関数を既に書き直していたので、直感で次のように書き直しました。

getCurrentPlayerM _ = do state <- get
                         return (players state ! on_turn state)

そして確かに、それは機能します!しかし、もちろん getCurrentPlayerM () として呼び出す必要があり、それを行うのは少しばかげています。そもそも言い争いは避けたかった!

追加の驚き: ghci でそのタイプを見ると、

getCurrentPlayerM :: MonadState GameState m => t -> m P.Player

しかし、コードでそれを明示的に設定しようとすると、別のエラーが発生します。「制約 MonadState GameState m の非型変数引数」と、それを許可する言語拡張の提案です。私の GameState は型であり、型クラスではないためだと思いますが、実際には受け入れられているのに、それについて明示しようとすると受け入れられない理由は、もっと混乱しています。

要約すると:

  1. Stateモナドにnullary関数を書けないのはなぜですか?
  2. 回避策関数が実際に持っている型を宣言できないのはなぜですか?
4

1 に答える 1

13

問題は、関数の型シグネチャを記述せず、モノモーフィズムの制限が適用されることです。

あなたが書くとき:

getCurrentPlayerM = ...

型宣言なしで最上位の単項制約値定義を記述しているため、Haskell コンパイラは定義の型を推測しようとします。ただし、モノモーフィズムの制限 (文字通り: 単一形状の制限) は、推論された型の制約を持つすべてのトップレベルの定義が具体的な型に解決されなければならない、つまり、ポリモーフィックであってはならないことを示しています。


私の言いたいことを説明するために、次の簡単な例を見てみましょう。

pi = 3.14

ここではpi、型なしで定義するので、GHC は型Fractional a => aを推論します。つまり、「a分数のように扱える限り、任意の型」です。ただし、この型は問題がpiあります。定数のように見えても定数ではないことを意味するからです。なんで?の値は、pi必要なタイプに応じて再計算されるためです。

があれば(2::Double) + pipiになりますDouble。があれば(3::Float) + pipiになりますFloat。したがって、 が使用されるたびpiに、再計算する必要があります (可能なすべての分数型の代替バージョンを格納することはできないためpiですね?)。これは単純なリテラルの場合は問題ありません3.14が、より多くの小数が必要でpi、それを計算する手の込んだアルゴリズムを使用した場合はどうなるでしょうか? piが使用されるたびに再計算されることは望ましくありません。

これが Haskell Report が、この問題を回避するために、最上位の単項型制約定義が単一の型 (単相型) を持つ必要があると述べている理由です。この場合、の型をpi取得します。必要に応じて、キーワードを使用してデフォルトの数値型を変更できます。defaultDoubledefault

default (Int, Float)

pi = 3.14 -- pi will now be Float

ただし、あなたの場合、推測された署名を取得しています。

getCurrentPlayerM :: MonadState GameState m => m P.Player

これは、「s を格納する状態モナドについてGameState、プレーヤーを取得する」ことを意味します。ただし、モノモーフィズムの制限が適用されるため、Haskell は、 の具象型を選択することによって、この型を非ポリモーフィックにしようとすることを余儀なくされmます。ただし、数値の場合のように状態モナドのデフォルトの型がないため、それを見つけることができず、あきらめます。

関数に明示的な型シグネチャを与えたい場合:

getCurrentPlayerM :: MonadState GameState m => m P.Player

...しかし、FlexibleContextsこれを機能させるには、ファイルの先頭にこれを追加して、Haskell 言語拡張機能を追加する必要があります。

{-# LANGUAGE FlexibleContexts #-}

または、必要な状態モナドを明示的に指定できます。

getCurrentPlayerM :: State GameState P.Player

拡張機能を追加することで、モノモーフィズムの制限を無効にすることもできます。ただし、型シグネチャを追加する方がはるかに優れています。

{-# LANGUAGE NoMonomorphismRestriction #-}

PS。状態をパラメーターとして受け取る関数がある場合は、次を使用できます。

value <- gets getCurrentPlayer

また、状態モナドでレンズを使用することも検討する必要があります。これにより、暗黙的な状態の受け渡しのための非常にクリーンなコードを記述できます。

于 2012-06-02T22:07:34.777 に答える