web-dev-qa-db-ja.com

慣用的な効率的なHaskellの追加?

リストと短所演算子(:)は、Haskellでは非常に一般的です。短所は私たちの友達です。しかし、代わりにリストの最後に追加したい場合があります。

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

悲しいことに、これはnotそれを実装する効率的な方法ではありません。

私はHaskellで パスカルの三角形 を書きましたが、++ [x]アンチイディオムを使用する必要がありました。

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

私見、これは素敵な読みやすいパスカルの三角形とすべてですが、反イディオムは私を苛立たせます。最後に効率的に追加したい場合の慣用的なデータ構造について、誰かが私に説明できますか(理想的には、良いチュートリアルを教えてください)?このデータ構造とそのメソッドに、リストに近い美しさを期待しています。または、代わりに、この反イディオムが実際にこのケースにそれほど悪くない理由を私に説明してください(あなたがそのように信じている場合)。


[編集]私が一番好きな答えは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をクラスにする必要があります。これにより、他の構造体は、Preludeから隠したり、修飾したりせずに、zipWithなどのメソッドを使用できます。 :P

35
Dan Burton

標準のSequenceにはO(1) '両端'からの加算用]とO(log(min(n1、n2)))があります。

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

ただし、リストとの違いは、Sequenceが厳密であるということです。

16
Ed'ka

あなたは怠惰な言語で働いているので、貧弱な漸近解析に見えるものは実際にはそうではないかもしれないことを覚えておいてください。厳密な言語では、この方法でリンクリストの最後に追加するのは常にO(n)です。怠惰な言語では、実際にリストの最後までトラバースする場合にのみO(n)です。その場合、O(n) =とにかく努力。だから多くの場合、怠惰はあなたを救います。

これは保証ではありません...たとえば、k個の追加とそれに続くトラバーサルはO(nk)で実行されますが、O(n + k)である可能性があります)。結果がすぐに強制されたときの漸近的な複雑さの観点から単一操作のパフォーマンスを考えると、最終的に正しい答えが得られるとは限りません。

28
Chris Smith

この明示的な再帰のようなものは、追加の「アンチイディオム」を回避します。しかし、あなたの例ほど明確ではないと思います。

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]
10
David Powell

パスカルの三角形のコードでは、++ [x]は実際には問題ではありません。とにかく++の左側に新しいリストを作成する必要があるため、アルゴリズムは本質的に2次式です。 ++を回避するだけでは、漸近的に高速化することはできません。

また、この特定のケースでは、-O2をコンパイルすると、GHCのリスト融合ルールにより、++が通常作成するリストのコピーが削除されます(すべきです)。これは、zipWithが優れたプロデューサーであり、++が最初の引数で優れたコンシューマーであるためです。これらの最適化については、 GHCユーザーガイド で読むことができます。

8
lpsmith

ユースケースによっては、ShowSメソッド(関数合成を介して追加)が役立つ場合があります。

5
geekosaur

安価なappend(concat)とsnoc(右側のcons)が必要な場合は、HuckageのDListとも呼ばれるHughesリストが最も簡単に実装できます。それらがどのように機能するかを知りたい場合は、AndyGillとGrahamHuttonの最初のWorkerWrapperの論文を見てください。JohnHughesの元の論文はオンラインではないようです。他の人が上で言ったように、ShowSは文字列に特化したヒューズリスト/ DListです。

JoinListは、実装するのにもう少し作業が必要です。これはバイナリツリーですが、リストAPIを使用します-concatとsnocは安価であり、合理的にfmapできます:HackageのDListにはファンクターインスタンスがありますが、持ってはいけないと主張します-ファンクターインスタンスは内外に変形する必要があります通常のリスト。 JoinListが必要な場合は、独自にロールする必要があります。Hackageにあるものは私のものであり、効率的でも、適切に記述されていません。

Data.Sequenceには効率的な短所とsnocがあり、JoinListが遅いテイク、ドロップなどの他の操作に適しています。 Data.Sequenceの内部フィンガーツリー実装はツリーのバランスを取る必要があるため、appendはJoinListの同等のものよりも多くの作業を行います。実際には、Data.Sequenceの方が適切に記述されているため、追加用のJoinListよりもパフォーマンスが優れていると思います。

5
stephen tetley

別の方法は、無限のリストを使用するだけで連結をまったく回避することです。

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

私は必ずしもあなたのコードを「反イドマティック」とは呼びません。多くの場合、クリアな方が良い、たとえそれが数クロックサイクルを犠牲にすることを意味するとしても。

そして、あなたの特定のケースでは、最後の追加は実際にはbig-O 時間計算量を変更しません!式の評価

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

比例して時間がかかりますlength xsそしてそれを変えるような派手なシーケンスデータ構造はありません。どちらかといえば、定数係数のみが影響を受けます。

3

クリスオカサキは、この問題に対処するキューの設計をしています。彼の論文の15ページを参照してください http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf

コードを少し調整する必要があるかもしれませんが、リバースを使用してリストの2つの部分を保持すると、平均してより効率的に作業できます

また、誰かが効率的な操作でモナドリーダーにリストコードを入れました。確かに、あまりフォローしていませんでしたが、集中すれば理解できると思いました。モナドリーダー第17号のダグラスM.オークレアだったことが判明 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)
2
Tim Perry

@geekosaurのShowSアプローチの例を書きました。 preludeShowSの例がたくさんあります。

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

[編集] @Danのアイデアとして、私はzipWithSでnewRowを書き直しました。

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
1

[]からリストを作成する関数としてリストを表すことができます

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はData.Listにあります):

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

汎用ソリューションを探しているなら、これはどうですか?

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

これにより、マップの簡単な代替定義が得られます。

map = mapOnto []

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)
1
rampion