Home About
Haskell , Excel

Haskell / 再帰関数を foldl または foldr に直す

コーヒーメニューの価格改訂リストの作成で書いた再帰関数をリファクタリングします。

元の再帰関数

リファクタリング対象の関数は addMergedItems で、 MergedItem のリストを再帰的に Worksheet に追記していく処理です。

addMergedItems :: Worksheet -> [MergedItem] -> RowIndex -> Worksheet
addMergedItems sheet mergedItems rowIndex
  | (length mergedItems) == 0 = sheet
  | otherwise = addMergedItems sheet' (tail mergedItems) (rowIndex + 1)
  where
    sheet' =
      addCellValues
        sheet
        [(name mergedItem), (oldPrice mergedItem), (newPrice mergedItem)]
        rowIndex
    mergedItem = head mergedItems
    name :: MergedItem -> CellValue
    name (MergedItem v _ _) = v
    oldPrice :: MergedItem -> CellValue
    oldPrice (MergedItem _ v _) = v
    newPrice :: MergedItem -> CellValue
    newPrice (MergedItem _ _ v) = v

処理内容は以下です。

再帰するたびに変化するのは sheet と rowIndex です。 そして、MergedItem リストを先頭から順に処理していく、流れです。

foldl とか foldr を使ってこの処理をコードしてみます。

foldl

> :type foldl
(a -> b -> a) -> a -> [b] -> a

実装:

addMergedItems :: Worksheet -> [MergedItem] -> RowIndex -> Worksheet
addMergedItems sheet mergedItems rowIndex = fst results
  where
    results = foldl f (sheet, rowIndex) mergedItems
    f :: (Worksheet, RowIndex) -> MergedItem -> (Worksheet, RowIndex)
    f sheetAndRowIndex mergedItem = (nextSheet, nextRowIndex)
      where
        nowSheet = fst sheetAndRowIndex
        nowRowIndex = snd sheetAndRowIndex
        nextSheet = addCellValues nowSheet cellValues nowRowIndex
        nextRowIndex = (snd sheetAndRowIndex) + 1
        cellValues =
          [(name mergedItem), (oldPrice mergedItem), (newPrice mergedItem)]
    name :: MergedItem -> CellValue
    name (MergedItem v _ _) = v
    oldPrice :: MergedItem -> CellValue
    oldPrice (MergedItem _ v _) = v
    newPrice :: MergedItem -> CellValue
    newPrice (MergedItem _ _ v) = v

ポイント:

    results = foldl f (sheet, rowIndex) mergedItems
    f :: (Worksheet, RowIndex) -> MergedItem -> (Worksheet, RowIndex)
    f sheetAndRowIndex mergedItem = (nextSheet, nextRowIndex)

foldr

> type: foldr
(a -> b -> b) -> b -> [a] -> b

実装:

addMergedItems' :: Worksheet -> [MergedItem] -> RowIndex -> Worksheet
addMergedItems' sheet mergedItems rowIndex = fst results
  where
    results = foldr f (sheet, rowIndex) mergedItems
    f :: MergedItem -> (Worksheet, RowIndex) -> (Worksheet, RowIndex)
    f mergedItem sheetAndRowIndex = (nextSheet, nextRowIndex)
      where
        nowSheet = fst sheetAndRowIndex
        nowRowIndex = snd sheetAndRowIndex
        nextSheet = addCellValues nowSheet cellValues nowRowIndex
        nextRowIndex = (snd sheetAndRowIndex) + 1
        cellValues =
          [(name mergedItem), (oldPrice mergedItem), (newPrice mergedItem)]
    name :: MergedItem -> CellValue
    name (MergedItem v _ _) = v
    oldPrice :: MergedItem -> CellValue
    oldPrice (MergedItem _ v _) = v
    newPrice :: MergedItem -> CellValue
    newPrice (MergedItem _ _ v) = v

ポイント:

    results = foldr f (sheet, rowIndex) mergedItems
    f :: MergedItem -> (Worksheet, RowIndex) -> (Worksheet, RowIndex)
    f mergedItem sheetAndRowIndex = (nextSheet, nextRowIndex)

リストを右から処理するので、foldl と出力される順が上下逆になります。 コーヒーメニューアイテムの処理では出力順は問題になりませんが、もし foldl と同じにしたいのであれば、 [MergedItem]reverse してから foldr すればよい。

まとめ

foldl や foldr を使うことで、独自に再帰関数を書かなくても済む。 foldl や foldr で繰り返しがおきるときに何が変化して、何を蓄積していけばよいのかを見極める必要がある。