Идиоматический эффективный Haskell append?

список и зэками (:) очень распространены в Haskell. Зэки - наши друзья. Но иногда я хочу добавить в конец списка.

xs `append` x = xs ++ [x]

это, к сожалению,не эффективный способ его реализации.

Я написал треугольник Паскаля в Haskell, но я должен был использовать ++ [x] анти-идиома:

ptri = [1] : mkptri ptri
mkptri (row:rows) = newRow : mkptri rows
    where newRow = zipWith (+) row (0:row) ++ [1]

имхо, это прекрасный читаемый треугольник Паскаля и все такое, но анти-идиома раздражает меня. Может кто-нибудь объясните мне (и, в идеале, укажите мне хороший учебник) о том, что такое идиоматическая структура данных для случаев, когда вы хотите добавить к концу эффективно? Я надеюсь на близкую к списку красоту в этой структуре данных и ее методах. Или, наоборот, объясните мне, почему эта анти-идиома на самом деле не так уж плоха для этого случая (если вы считаете, что это так).


[edit] ответ, который мне нравится больше всего, это Data.Sequence, который действительно имеет "близкую к списку красоту"."Не знаю, как я почувствуйте необходимую строгость операций. Новые предложения и новые идеи всегда приветствуются.

import Data.Sequence ((|>), (<|), zipWith, singleton)
import Prelude hiding (zipWith)

ptri = singleton 1 : mkptri ptri

mkptri (seq:seqs) = newRow : mkptri seqs
    where newRow = zipWith (+) seq (0 <| seq) |> 1

теперь нам просто нужно, чтобы List был классом, чтобы другие структуры могли использовать его методы, такие как zipWith не скрывая его от прелюдии или квалификации. : P

12 ответов


стандартный Sequence имеет O(1) для сложения с "обоих концов" и O(log(min (n1,n2))) для общей конкатенации:

http://hackage.haskell.org/packages/archive/containers/latest/doc/html/Data-Sequence.html

отличие от списков, хотя это Sequence строго


имейте в виду, что то, что выглядит плохой асимптотикой, на самом деле не может быть, потому что вы работаете на ленивом языке. На строгом языке добавление к концу связанного списка таким образом всегда будет O(n). На ленивом языке Это O(n), только если вы действительно пройдете до конца списка, и в этом случае вы все равно потратили бы O (n) усилия. Поэтому во многих случаях лень спасает вас.

Это не гарантия... например, K добавляет, а затем обход будет по-прежнему запустите в O(nk), где это могло быть O (n+k). Но это несколько меняет картину. Думать о производительности отдельных операций с точки зрения их асимптотической сложности, когда результат сразу же не всегда даст вам правильный ответ в конце.


что-то вроде этой явной рекурсии позволяет избежать добавления "анти-идиомы". Хотя, я не думаю, что это так ясно, как ваш пример.

ptri = []:mkptri ptri
mkptri (xs:ys) = pZip xs (0:xs) : mkptri ys
    where pZip (x:xs) (y:ys) = x+y : pZip xs ys
          pZip [] _ = [1]

в вашем коде для треугольника Паскаля ++ [x] на самом деле не проблема. Поскольку вам все равно нужно создать новый список слева от++, ваш алгоритм по своей сути квадратичен; вы не можете сделать его асимптотически быстрее, просто избегая ++.

кроме того, в этом конкретном случае, когда вы компилируете-O2, правила слияния списка GHC (должны) исключить копию списка, который обычно создает++. Это потому, что zipWith-хороший производитель, а ++ - хороший потребитель это первый аргумент. Вы можете прочитать об этих оптимизациях в пользователей с GHC-х.


в зависимости от вашего варианта использования,ShowS метод (добавление через композицию функций) может быть полезным.


Если вы просто хотите дешево добавить (concat) и snoc (минусы справа), список Хьюза, также называемый DList на Hackage, является самым простым в реализации. Если вы хотите знать, как они работают, посмотрите на первую рабочую бумагу Энди Гилла и Грэма Хаттона, оригинальная бумага Джона Хьюза, похоже, не в сети. Как говорили другие выше, шоу - это строка, специализированная Hughes list / DList.

JoinList-это немного больше работы для реализации. Это двоичное дерево, но со списком API-concat и snoc дешевы, и вы можете разумно fmap это: DList на Hackage имеет экземпляр функтора, но я утверждаю, что он не должен иметь - экземпляр функтора должен метаморфировать В и из обычного списка. Если вам нужен JoinList, вам нужно будет свернуть свой собственный - тот, который на Hackage, мой, и он не эффективен и не хорошо написан.

данные.Последовательность имеет эффективные минусы и snoc, и хороша для других деятельностей - взятий, падений etc. для этого JoinList медленный. Потому что внутренний палец древовидная реализация данных.Последовательность должна сбалансировать дерево, добавить больше работы, чем его эквивалент JoinList. На практике потому что данные.Последовательность лучше написана, я ожидаю, что она все еще выполняет мой JoinList для append.


другой способ избежать конкатенации вообще, просто используя бесконечные списки:

ptri = zipWith take [0,1..] ptri'
  where ptri' = iterate stepRow $ repeat 0
        stepRow row = 1 : zipWith (+) row (tail row)

Я бы не обязательно назвал ваш код "антиидоматическим". Часто,понятнее, лучше, даже если это означает пожертвовать несколько тактов.

и в вашем конкретном случае приложение в конце фактически не изменяет big-O сложность! Оценка выражения

zipWith (+) xs (0:xs) ++ [1]

займет время пропорционально length xs и никакая причудливая структура данных последовательности не изменит это. Во всяком случае, только постоянный фактор будет пострадавших.


Chris Okasaki имеет дизайн для очереди, которая решает эту проблему. См. стр. 15 его диссертации http://www.cs.cmu.edu / ~rwh / тезисы / okasaki.pdf

возможно, вам придется немного адаптировать код, но некоторое использование reverse и сохранение двух частей списка позволяет работать более эффективно в среднем.

кроме того, кто-то поставил некоторый код списка в читателе монады с эффективными операциями. Признаюсь, я не совсем понял, но думал, что смогу. разберусь, если сконцентрируюсь. Оказывается, это был Дуглас М. Оклер в Monad Reader issue 17 http://themonadreader.files.wordpress.com/2011/01/issue17.pdf


Я понял, что приведенный выше ответ непосредственно не касается вопроса. Итак, для хихиканья, вот мой рекурсивный ответ. Не стесняйтесь разорвать его на части-это не красиво.

import Data.List 

ptri = [1] : mkptri ptri

mkptri :: [[Int]] -> [[Int]]
mkptri (xs:ys) =  mkptri' xs : mkptri ys

mkptri' :: [Int] -> [Int]
mkptri' xs = 1 : mkptri'' xs

mkptri'' :: [Int] -> [Int]
mkptri'' [x]        = [x]
mkptri'' (x:y:rest) = (x + y):mkptri'' (y:rest)

если вы ищете решение общего назначения, то как насчет этого:

mapOnto :: [b] -> (a -> b) -> [a] -> [b]
mapOnto bs f = foldr ((:).f) bs

это дает простое альтернативное определение по карте:

map = mapOnto []

мы можем аналогичное определение для других функций на основе foldr, таких как zipWith:

zipOntoWith :: [c] -> (a -> b -> c) -> [a] -> [b] -> [c]
zipOntoWith cs f = foldr step (const cs)
  where step x g [] = cs
        step x g (y:ys) = f x y : g ys

снова выводя zipWith и zip довольно легко:

zipWith = zipOntoWith []
zip = zipWith (\a b -> (a,b))

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

ptri :: (Num a) => [[a]]
ptri = [] : map mkptri ptri
  where mkptri xs = zipOntoWith [1] (+) xs (0:xs)

Я написал пример @geekosaur это ShowS подход. Вы можете увидеть много примеров ShowS на прелюдия.

ptri = []:mkptri ptri
mkptri (xs:ys) = (newRow xs []) : mkptri ys

newRow :: [Int] -> [Int] -> [Int]
newRow xs = listS (zipWith (+) xs (0:xs)) . (1:)

listS :: [a] -> [a] -> [a]
listS [] = id
listS (x:xs) = (x:) . listS xs

[edit] как идея @Dan, я переписал newRow с помощью zipWithS.

newRow :: [Int] -> [Int] -> [Int]
newRow xs = zipWithS (+) xs (0:xs) . (1:)

zipWithS :: (a -> b -> c) -> [a] -> [b] -> [c] -> [c]
zipWithS z (a:as) (b:bs) xs =  z a b : zipWithS z as bs xs
zipWithS _ _ _ xs = xs

вы можете представить список как функцию для построения списка из []

list1, list2 :: [Integer] -> [Integer]
list1 = \xs -> 1 : 2 : 3 : xs
list2 = \xs -> 4 : 5 : 6 : xs

затем вы можете легко добавлять списки и добавлять в любой конец.

list1 . list2 $ [] -> [1,2,3,4,5,6]
list2 . list1 $ [] -> [4,5,6,1,2,3]
(7:) . list1 . (8:) . list2 $ [9] -> [7,1,2,3,8,4,5,6,9]

вы можете переписать zipWith, чтобы вернуть эти частичные списки:

zipWith' _ [] _ = id
zipWith' _ _ [] = id
zipWith' f (x:xs) (y:ys) = (f x y :) . zipWith' f xs ys

и теперь вы можете написать ptri как:

ptri = [] : mkptri ptri
mkptri (xs:yss) = newRow : mkptri yss
    where newRow = zipWith' (+) xs (0:xs) [1]

принимая это дальше, вот один лайнер, который более симметричен:

ptri = ([] : ) . map ($ []) . iterate (\x -> zipWith' (+) (x [0]) (0 : x [])) $ (1:)

или это еще проще:

ptri = [] : iterate (\x -> 1 : zipWith' (+) (tail x) x [1]) [1]

или без zipWith' (mapAccumR в данных.Список):

ptri = [] : iterate (uncurry (:) . mapAccumR (\x x' -> (x', x+x')) 0) [1]