Как лениво Haskell `++`?

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

import Data.List
swapAt n = f . splitAt n where f (a,b) = b++a
minimumrotation x = minimum $ map (\i -> swapAt i x) $ elemIndices (minimum x) x

Я бы предположил, что я должен использовать Data.Vector вместо списков, потому что Data.Vector предоставляет операции на месте, возможно, просто манипулируя некоторыми индексами в исходных данных. На самом деле мне не нужно беспокоиться о том, чтобы отслеживать индексы, чтобы избежать избыточного копирования, верно?

Мне любопытно, как ++ влияет на оптимизацию. Я бы предположил, что это создает ленивый струнный тон, который никогда не добавляет, пока строка не будет прочитана так далеко. Ergo, a никогда не должен быть добавлен к b, когда минимум может устранить эту строку раньше, например, потому что она начинается с более поздней буквы. Правильно ли это?

Ответы

Ответ 1

xs ++ ys добавляет некоторые издержки во всех ячейках списка из xs, но как только он достигает конца xs, он освобождается - он просто возвращает ys.

Глядя на определение (++), можно понять, почему:

[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

i.e., он должен "перестроить" весь первый список по мере прохождения результата. Эта статья очень полезна для понимания того, как рассуждать о ленивом коде таким образом.

Ключом к пониманию является то, что добавление не выполняется сразу; новый связанный список поэтапно строится, сначала пройдя через все xs, а затем помещая ys, где будет [].

Итак, вам не нужно беспокоиться о достижении конца b и внезапном возникновении одноразовой стоимости "добавления" a; стоимость распределяется по всем элементам b.

Векторы - это совсем другое дело; они строги по своей структуре, поэтому даже рассмотрение только первого элемента xs V.++ ys несет на себе все накладные расходы, связанные с распределением нового вектора и копированием xs и ys на него - как на строгом языке. То же самое относится к изменяемым векторам (за исключением того, что стоимость возникает при выполнении операции, а не при форсировании результирующего вектора), хотя я думаю, что вам придется писать свою собственную операцию добавления с ними в любом случае. Вы можете представить кучу добавленных (неизменяемых) векторов как [Vector a] или похожих, если это проблема для вас, но это просто перемещает накладные расходы, когда вы сплющиваете его обратно в один вектор, и это звучит так, будто вы больше интересующихся изменчивыми векторами.

Ответ 2

Try

minimumrotation :: Ord a => [a] -> [a]
minimumrotation xs = minimum . take len . map (take len) $ tails (cycle xs)
  where
    len = length xs

Я ожидаю, что это будет быстрее, чем у вас, хотя индексный жонглирование на unboxed Vector или UArray, вероятно, будет еще быстрее. Но, действительно ли это узкое место?

Ответ 3

Если вы заинтересованы в быстрой конкатенации и быстрой splitAt, используйте Data.Sequence.

Я сделал некоторые стилистические модификации вашего кода, чтобы он выглядел скорее как идиоматический Haskell, но логика точно такая же, за исключением нескольких преобразований в и из Seq:

import qualified Data.Sequence as S
import qualified Data.Foldable as F

minimumRotation :: Ord a => [a] -> [a]
minimumRotation xs = F.toList
                   . F.minimum
                   . fmap (`swapAt` xs')
                   . S.elemIndicesL (F.minimum xs')
                   $ xs'
  where xs' = S.fromList xs
        swapAt n = f . S.splitAt n
          where f (a,b) = b S.>< a