web-dev-qa-db-ja.com

再帰的なHaskellコードで状態を追跡するためのパターン

私のHaskellコードで頻繁に見られるパターンは、リストのデータを使用して生成された運ばれた状態を持つリストの変換の要素ごとの再帰です。通常、これは次のようになります。

_doSomething :: (SomeA a, SomeB b) => [a] -> [b]
doSomething xs = doSomethingWithState xs []
    where
        doSomethingWithState [] _ = []
        doSomethingWithState (x:xs) state
            | someTest x state = someChange x : (doSomethingWithState xs newState)
            | otherwise = someOtherChange x : (doSomethingWithState xs newState)
_

たとえば、リストに表示される各要素の数を数え、_[1, 3, 3, 3, 4, 7, 7, 8, 8, 9]_などを[(9,1),(8,2),(7,2),(4,1),(3,3),(1,1)]に変換したいとします。

私はおそらく次のようなことをするでしょう:

_import Data.List
import Data.Maybe

counts :: (Eq a) => [a] -> [(a, Int)]
counts xs = countsWithState xs []
    where
        countsWithState [] state = state               -- End of list, stop here
        countsWithState (x:xs) state = countsWithState xs $ transformState state x
        transformState state x                         -- To get the state...
            | isNothing $ lookup x state = (x, 1) : state -- Add new elem if new
            | otherwise = incrElem x state             -- Increment elem if not
        incrElem x [] = []                             -- Should never be reached
        incrElem x ((index, value):elems)              -- Searching through list...
            | index == x = (index, (value+1)) : elems  -- Increment if found
            | otherwise = (index, value) : incrElem x elems -- Try next if not
_

より単純ですが非常に似た例で、リスト内のすべての要素の移動平均を維持しようとした場合、_[1, 7, 4, 18, 7, 1, 8, 2, 8, 6, 18, 12]_のようなものを_[1.0, 4.0, 4.0, 7.5, 7.4, 6.33..., 5.57..., 6.0, 6.22..., 6.2, 7.27..., 7.66...]_に変換します。ここで、出力リストのすべての要素は、その要素と入力リスト内の以前のすべての要素、私はこのようなことをするかもしれません:

_runningAvg :: (Fractional a) => [a] -> [a]
runningAvg xs = runningAvgWithState xs 0 1
    where
        runningAvgWithState [] _ _ = []
        runningAvgWithState (x:xs) currentSum currentElems
            = (currentSum + x) / currentElems
            : runningAvgWithState xs (currentSum + x) (currentElems + 1)
_

パターンが同じであることに注意してください。リストの再帰的関数を取り、状態を追加した非表示の修正バージョンの観点から定義し、各ラウンドで状態を変換し、必要に応じて計算結果を出力します。 このパターンは私のHaskellコードで常に現れます

より複雑なxWithState関数がショーを実行して不要な冗長性と複雑さを追加することなく、この種の動作を実装するより自然な方法はありますか?

2

あなたのdoSomethingは多かれ少なかれmapAccumLData.Listからですが、最後にアキュムレータ(つまり状態)を捨てました。つまり、次のように書くことができます。

doSomething :: [a] -> [b]
doSomething = snd . mapAccumL step []
  where step state x = (newState, newX)
          where newX | someTest x state = someChange x
                     | otherwise        = someOtherChange x
                newState = state

特に、移動平均は次のようになります。

runningAvg :: (Fractional a) => [a] -> [a]
runningAvg = snd . mapAccumL step (0,0)
  where step (total, n) x = ((total', n'), total' / fromIntegral n')
          where total' = total + x
                n' = n + 1

入力と同じ「形状」のリストを生成しないため、カウントの例は多少異なります。代わりに、「状態」はカウントのセットであり、最後に最後の「状態」(カウントの最終セット)を返します。他の人が指摘したように、これは単なるfoldlです。

count :: (Eq a) => [a] -> [(a, Int)]
count = foldl step []
  where step cnts x = bumpCount x cnts

これは、一連のカウントを連想リストとして維持することでbumpCountが粗雑になるという事実がなければ、簡単です。 @amonのバージョンのbumpCount(つまり、count')は問題ないようですが、次のように書くこともできます。

bumpCount :: (Eq a) => a -> [(a, Int)] -> [(a, Int)]
bumpCount x = foldr step [(x,1)] . tails
  where step ((y,n):rest) acc | x == y    = (y,n+1) : rest
                              | otherwise = (y,n)   : acc
        step [] acc = acc

ちなみに、このbumpCountフォールドは実際にはかなり素晴らしいです。 「フォールド」であるにもかかわらず、一致するカウントが見つかると、リストの検索を停止します。

2
K. A. Buhr

LISP、Haskell、Scalaのいずれを使用していても、関数型プログラミングでは、メインの作業を1つ以上のアキュムレータ引数を持つネストされた再帰関数に委任するパターンは完全に正常です。コードを再帰的に表現することに慣れている限り、変更可能な状態を追跡する必要はありません。

Haskellでは、「パブリック」関数fooの内部関数はしばしばfoo'(foo-prime)と呼ばれることに注意してください。

OOP Javaのような言語での実際の実装の分割と同じパブリックインターフェイスと同じように、たとえば次のようになります。

class Foo {

  public int doTheThing(int x, int y) {
    // validate x and y
    doTheThing(x, y, new HashMap<>());
  }

  private int doTheThing(int x, int y, HashMap<Integer, List<Integer>> cache) {
   ...
  }
}

しかし、Haskellに戻ると、「ある状態を追跡しながら各要素に対して何かを行う」とは、スキャン、折りたたみ、または縮小の操作と同様に、抽象化できる繰り返しのパターンであることに注意するかもしれません。たとえば、counts関数は次のように記述できます(transformStateincrElemを組み合わせて、incrElemが規範事例):

counts :: (Eq a) => [a] -> [(a, Int)]
counts = foldr counts' []
  where counts' x [] = [(x, 1)]
        counts' x ((y, n):rest) = if x == y then (x, n + 1):rest
                                            else (y, n):(counts' x rest)

counts'関数も折り畳みの一種ですが、1つにまとめるのは面倒です。

1
amon