Как подсчитать количество четных целых чисел в списке в Haskell с помощью State monad?

#haskell

#haskell

Вопрос:

Я пытаюсь изучить некоторые основы государственной монады в Haskell, создавая свои собственные примеры.

Рассмотрим простой пример, в котором я хочу подсчитать количество четных целых чисел в массиве целых чисел. Конечно, это можно сделать очень легко, используя чистые функции, но я хотел попробовать маршрут с круговым движением по State monad, где мы сохраняем счетчик, который продолжает увеличиваться для каждого проверенного элемента.

Вот частичная (но явно неправильная) попытка, которую мне удалось придумать до сих пор.

 import Control.Monad.State

f' :: [Int] -> State Int [Int]
f' []     = state (s -> ([],s)) 
f' (x:xs) = if x `mod` 2 == 0 then state (s -> ((x:xs), s 1)) -- how can I fix this line? 
            else f' xs
  

Этот код компилируется, но явно не дает правильного ответа. Как тогда я могу исправить этот код, чтобы сделать что-то похожее на следующий код Python

 counter = 0 # incremented when we encounter an even integer.
for elt in integer_list:
   if elt % 2 == 0 :
      counter = counter   1 
  

Ответ №1:

Другой ответ начинается с нуля, чтобы создать реализацию. Я думаю, также стоит внести минимальные изменения в существующий код, чтобы сделать его разумным. Мы даже сохраним ваш существующий тип — хотя в другом ответе предлагается его изменить, я думаю, что это приемлемо (если не здорово).

На мой взгляд, реальная проблема заключается в том, что вы выполнили рекурсию только в одной ветви вашего if . Чего мы действительно хотим, так это повторить, является ли текущий элемент четным. Так что:

 f' (x:xs) = do
    if x `mod` 2 == 0 then state (s -> ((), s 1)) -- increase the count by one
                      else state (s -> ((), s  )) -- don't increase the count by one
    rest <- f' xs -- compute the answer for the rest of the list
    return (x:rest) -- reconstruct the answer for the whole list
  

Мы можем проверить, что он делает правильные вещи в ghci:

 > runState (f' [1..5]) 0
([1,2,3,4,5],2)
  

Это лишь самое маленькое изменение, которое вы можете внести, чтобы ваша идея реализации заработала.

Исходя из этого, я бы предложил ряд рефакторингов. Во-первых, ваше повсеместное использование state — это запах кода. Вместо этого я бы написал различные варианты использования таким образом:

 f' [] = return []
f' (x:xs) = do
    if x `mod` 2 == 0 then modify ( 1) else return ()
    rest <- f' xs
    return (x:rest)
  

Отсюда я бы использовал even функцию в условном выражении и заметил, что when функция реализует операцию «выполнить какое-либо действие или return () «. Итак:

 f' [] = return []
f' (x:xs) = do
    when (even x) (modify ( 1))
    rest <- f' xs
    return (x:rest)
  

Кроме того, у нас фактически есть комбинатор для выполнения монадического действия над каждым элементом списка; то есть mapM . Таким образом, мы можем превратить вышеупомянутую явную рекурсию в неявную таким образом:

 f' xs = mapM (x -> when (even x) (modify ( 1)) >> return x) xs
  

Наконец, я думаю, что немного странно, что функция возвращает список, который она использовала. Само по себе не однотипно, как и предыдущие возражения, но, возможно, не то, что вы хотите. Если окажется, что вы не планируете использовать полученный список в каких-либо последующих вычислениях, будет эффективнее выбросить его по ходу дела; и mapM_ комбинатор делает это. Итак:

 f' :: [Int] -> State Int ()
f' xs = mapM_ (x -> when (even x) (modify ( 1))) xs
  

На данный момент я бы счел f' довольно хорошей реализацией предложенной вами идеи.

Комментарии:

1. может быть, еще лучше следующим образом 😉 f’ :: [Int] -> State Int () f’ = mapM_ (x -> когда (четный x) (изменить ( 1)))

2. Большое спасибо за подробные объяснения!

Ответ №2:

Давайте вернемся к чертежной доске. Фактическая функция, которую вы хотите использовать, выглядит примерно так

 countEven :: [Int] -> Int
countEven xs = runStateMagic xs
  

где runStateMagic использует некоторые State скрытые в его глубинах. Как может выглядеть эта функция? Ну, он должен использовать либо execState или evalState . Поскольку нас интересует только состояние (иначе говоря, наше текущее количество чисел), давайте заменим runStateMagic на execState :

 countEven :: [Int] -> Int
countEven xs = execState someState 0
  

Теперь execState тип исправляет наше stateFunc значение State Int a . Фактический тип значения состояния произвольный, поскольку мы все равно не собираемся его использовать. Итак, что же someState делать? Вероятно, это должно работать в списке и использоваться modify' ( 1) , если у нас есть четное число. Давайте напишем помощник для этого:

 increaseIfEven :: Int -> State Int ()
increaseIfEven n
  | even n    = modify' inc
  | otherwise = return ()
  

Теперь это изменит состояние, если число было четным. Все, что нам нужно сделать, это применить это к каждому элементу в списке. Поэтому для списка xs мы можем просто сделать

 mapM_ increaseIfEven xs
  

Помните, mapM_ :: (a -> m b) -> [a] -> m () . Но в нашем случае, то m есть State Int , так что он уже содержит наш счетчик.

В целом, мы получаем

 countEven :: [Int] -> Int
countEven xs = execState (mapM_ increaseIfEven xs) 0
  

Но имейте в виду: важной частью было исправить тип исходной функции, f' .

Комментарии:

1. @ Zeta Вау! Большое вам спасибо! Хотел бы я принять оба ответа. Я отметил другой, поскольку он построен на моем решении, но этот ответ тоже был чрезвычайно полезен. Еще раз спасибо за подробный ответ!