9

Haskellの背後にある数学の基礎について読んでいます-クロージャを使用して関数の状態を保存する方法について学びました。

Haskellがクロージャを許可するかどうか、そしてそれらは純粋関数ではないのでどのように機能するのか疑問に思いました。

関数がクローズオーバー状態を変更すると、同じ入力で異なる出力を提供できるようになります。

これはHaskellではどうして問題にならないのですか?最初に値を割り当てた後、変数を再割り当てできないためですか?

4

3 に答える 3

10

Haskell で実際にクロージャーをシミュレートすることはできますが、あなたが考える方法ではありません。まず、クロージャ タイプを定義します。

data Closure i o = Respond (i -> (o, Closure i o ))

iこれは、各「ステップ」で、 type の応答を計算するために使用されるtype の値を取る型を定義しますo

したがって、空の入力と整数での回答を受け入れる「クロージャー」を定義しましょう。

incrementer :: Closure () Int

このクロージャの動作は、リクエストごとに異なります。シンプルに保ち、最初の応答に対して 0 で応答し、後続の要求ごとに応答をインクリメントするようにします。

incrementer = go 0 where
    go n = Respond $ \() -> (n, go (n + 1))

その後、クロージャーを繰り返しクエリできます。これにより、結果と新しいクロージャーが得られます。

query :: i -> Closure i o -> (o, Closure i o)
query i (Respond f) = f i

上記の型の後半は、Stateモナドである Haskell の一般的なパターンに似ていることに注意してください。

newtype State s a = State { runState :: s -> (a, s) }

からインポートできますControl.Monad.Statequeryしたがって、このStateモナドでラップできます。

query :: i -> State (Closure i o) o
query i = state $ \(Respond f) -> f i

...そして今、Stateモナドを使ってクロージャを問い合わせる一般的な方法があります:

someQuery :: State (Closure () Int) (Int, Int)
someQuery = do
    n1 <- query ()
    n2 <- query ()
    return (n1, n2)

クロージャーを渡して、何が起こるか見てみましょう。

>>> evalState someQuery incrementer
(0, 1)

任意のパターンを返す別のクロージャを書きましょう:

weirdClosure :: Closure () Int
weirdClosure = Respond (\() -> (42, Respond (\() -> (666, weirdClosure))))

...そしてそれをテストします:

>>> evalState someQuery weirdClosure
(42, 666)

さて、手でクロージャーを書くのはかなり厄介に思えます。do記法を使ってクロージャを記述できたらいいと思いませんか? まあ、できます!クロージャ タイプに変更を加える必要があるのは 1 つだけです。

data Closure i o r = Done r | Respond (i -> (o, Closure i o r))

Monadこれで、 のインスタンス (からControl.Monad) を定義できますClosure i o

instance Monad (Closure i o) where
    return = Done
    (Done r) >>= f = f r
    (Respond k) >>= f = Respond $ \i -> let (o, c) = k i in (o, c >>= f)

そして、単一のリクエストに対応する便利な関数を書くことができます:

answer :: (i -> o) -> Closure i o ()
answer f = Respond $ \i -> (f i, Done ())

...これを使用して、古いクロージャーをすべて書き換えることができます。

incrementer :: Closure () Int ()
incrementer = forM_ [1..] $ \n -> answer (\() -> n)

weirdClosure :: Closure () Int r
weirdClosure = forever $ do
    answer (\() -> 42)
    answer (\() -> 666)

ここで、クエリ関数を次のように変更します。

query :: i -> StateT (Closure i o r) (Either r) o
query i = StateT $ \x -> case x of
    Respond f -> Right (f i)
    Done    r -> Left  r

...そしてそれを使用してクエリを記述します。

someQuery :: StateT (Closure () Int ()) (Either ()) (Int, Int)
someQuery = do
    n1 <- query ()
    n2 <- query ()
    return (n1, n2)

今すぐテストしてください!

>>> evalStateT someQuery incrementer
Right (1, 2)
>>> evalStateT someQuery weirdClosure
Right (42, 666)
>>> evalStateT someQuery (return ())
Left ()

しかし、私はまだそれを真にエレガントなアプローチとは考えていないので、クロージャーとそのコンシューマーを記述するためのより一般的でより構造化された方法として、恥知らずに私のProxy型を my にプラグインすることで締めくくります。type は一般化されたクロージャーを表し、 は一般化されたクロージャーpipesのコンシューマーを表します。ServerClient

于 2012-09-16T00:30:36.843 に答える
8

クロージャは、関数に「追加の変数」を追加するだけなので、「通常の」変数よりも、それらを使用して実行できることはありません。つまり、状態を変更しないでください。

続きを読む: クロージャ(Haskell内)

于 2012-09-15T21:28:53.143 に答える
1

他の人が言ったように、Haskell はクロージャの「状態」を変更することを許可していません。これにより、関数の純粋性を損なう可能性のあることを行うことができなくなります。

于 2012-09-17T15:25:42.987 に答える