Как я могу сбросить с государством в Haskell?

У меня есть простая функция (фактически используется для некоторых задач проекта Euler). Превращает список цифр в десятичное число.

fromDigits :: [Int] -> Integer
fromDigits [x] = toInteger x
fromDigits (x:xs) = (toInteger x) * 10 ^ length xs + fromDigits xs

Я понял, что тип [Int] не идеально. fromDigits должен иметь возможность принимать другие входные данные, например, последовательности, может быть, даже foldables...

Моя первая идея состояла в том, чтобы заменить вышеприведенный код на "складывание с состоянием". Какова правильная (= минимальная) категория Haskell для вышеуказанной функции?

6 ответов

Решение

Вы должны избегать использования length и напишите свою функцию, используя foldl (или же foldl'):

fromDigits :: [Int] -> Integer
fromDigits ds = foldl (\s d -> s*10 + (fromIntegral d)) 0 ds

Из этого должно быть понятно обобщение на любой Foldable.

Во-первых, сворачивание уже связано с переносом состояния. Foldable это именно то, что вы ищете, нет необходимости State или другие монады.

Во-вторых, было бы более естественно определить базовый случай в пустых списках, а затем случай для непустых списков. В настоящее время функция не определена в пустых списках (хотя она будет полностью действительной). И обратите внимание, что [x] это просто сокращение для x : [],

В текущей форме функция была бы почти выразимой, используя foldr, Однако в пределах foldl список или его части недоступны, поэтому вы не можете вычислить length xs, (Вычислительный length xs на каждом шаге также делает всю функцию излишне O (n ^ 2).) Но этого можно легко избежать, если вы переделаете процедуру, чтобы использовать список наоборот. Новая структура функции может выглядеть так:

fromDigits' :: [Int] -> Integer
fromDigits' = f 0
  where
    f s []     = s
    f s (x:xs) = f (s + ...) xs

После этого попробуйте использовать foldl выражать f и, наконец, заменить его на Foldable.foldl,

Лучший способ решить эту проблему - составить список ваших полномочий 10. Это довольно просто, используя iterate:

powersOf :: Num a => a -> [a]
powersOf n = iterate (*n) 1

Тогда вам просто нужно умножить эти степени 10 на их соответствующие значения в списке цифр. Это легко сделать с zipWith (*), но вы должны убедиться, что это в правильном порядке в первую очередь. Это просто означает, что вы должны переупорядочить свои цифры так, чтобы они были в порядке убывания, а не в порядке возрастания:

zipWith (*) (powersOf 10) $ reverse xs

Но мы хотим вернуть Integerне Intтак что давайте через map fromIntegral там

zipWith (*) (powersOf 10) $ map fromIntegral $ reverse xs

И все, что осталось, это подвести их

fromDigits :: [Int] -> Integer
fromDigits xs = sum $ zipWith (*) (powersOf 10) $ map fromIntegral $ reverse xs

Или для фанатов без очков

fromDigits = sum . zipWith (*) (powersOf 10) . map fromIntegral . reverse

Теперь вы также можете использовать сложение, которое в основном является чистым циклом for, где функция - это тело вашего цикла, начальное значение - это, ну, в общем, начальное состояние, а список, который вы предоставляете, - это значения, по которым вы циклически выполняете, В этом случае ваше состояние является суммой и на какую власть вы опираетесь. Мы могли бы создать свой собственный тип данных, чтобы представить это, или мы могли бы просто использовать кортеж, в котором первый элемент - это текущая сумма, а второй - текущая мощность:

fromDigits xs = fst $ foldr go (0, 1) xs
    where
        go digit (s, power) = (s + digit * power, power * 10)

Это примерно эквивалентно коду Python

def fromDigits(digits):
    def go(digit, acc):
        s, power = acc
        return (s + digit * power, power * 10)

    state = (0, 1)
    for digit in digits:
        state = go(digit, state)
    return state[0]

Такая простая функция может нести все свое состояние в своих голых аргументах. Возьмите аргумент аккумулятора, и операция станет тривиальной.

fromDigits :: [Int] -> Integer
fromDigits xs = fromDigitsA xs 0  # 0 is the current accumulator value

fromDigitsA [] acc = acc
fromDigitsA (x:xs) acc = fromDigitsA xs (acc * 10 + toInteger x)

Если вы хотите "свернуть с состоянием", вероятно, Traversable - это та абстракция, которую вы ищете. Одним из методов, определенных в классе Traversable, является

traverse :: Applicative f => (a -> f b) -> t a -> f (t b)

По сути, traverse принимает "функцию с состоянием" типа a -> f b и применяет его к каждой функции в контейнере t a, в результате чего в контейнере f (t b), Вот, f может быть State, и вы можете использовать траверс с функцией типа Int -> State Integer (), Это создаст бесполезную структуру данных (список единиц в вашем случае), но вы можете просто отказаться от нее. Вот решение вашей проблемы с использованием Traversable:

import Control.Monad.State
import Data.Traversable

sumDigits :: Traversable t => t Int -> Integer
sumDigits cont = snd $ runState (traverse action cont) 0
  where action x = modify ((+ (fromIntegral x)) . (* 10))

test1 = sumDigits [1, 4, 5, 6]

Однако, если вам действительно не нравится создавать структуру данных, отброшенных, вы можете просто использовать Foldable с несколько хитрым Monoid реализация: хранить не только вычисленный результат, но и 10^n, где n это количество цифр, преобразованных в это значение. Эта дополнительная информация дает вам возможность объединить два значения:

import Data.Foldable
import Data.Monoid

data Digits = Digits
  { value :: Integer
  , power :: Integer
  }

instance Monoid Digits where
  mempty = Digits 0 1
  (Digits d1 p1) `mappend` (Digits d2 p2) =
    Digits (d1 * p2 + d2) (p1 * p2)

sumDigitsF :: Foldable f => f Int -> Integer
sumDigitsF cont = value $ foldMap (\x -> Digits (fromIntegral x) 10) cont

test2 = sumDigitsF [0, 4, 5, 0, 3]

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

Если вы действительно решили использовать правильную фолд для этого, вы можете комбинировать расчет length xs с таким расчетом (взять на себя смелость определения fromDigits [] = 0):

fromDigits xn = let (x, _) = fromDigits' xn in x where
    fromDigits' [] = (0, 0)
    fromDigits' (x:xn) = (toInteger x * 10 ^ l + y, l + 1) where
        (y, l) = fromDigits' xn

Теперь должно быть очевидно, что это эквивалентно

fromDigits xn = fst $ foldr (\ x (y, l) -> (toInteger x * 10^l + y, l + 1)) (0, 0) xn

Шаблон добавления дополнительного компонента или результата к вашему аккумулятору и его отбрасывания после возвращения сгиба является очень общим, когда вы переписываете рекурсивные функции с использованием сгибов.

Сказав это, foldr с функцией, которая всегда строга во втором параметре, это действительно очень плохая идея (чрезмерное использование стека, возможно переполнение стека в длинных списках), и вам действительно следует написать fromDigits как foldl как некоторые другие ответы предложили.

Другие вопросы по тегам