私の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
関数がショーを実行して不要な冗長性と複雑さを追加することなく、この種の動作を実装するより自然な方法はありますか?
あなたのdoSomething
は多かれ少なかれmapAccumL
Data.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
フォールドは実際にはかなり素晴らしいです。 「フォールド」であるにもかかわらず、一致するカウントが見つかると、リストの検索を停止します。
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
関数は次のように記述できます(transformState
とincrElem
を組み合わせて、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つにまとめるのは面倒です。