我发现Haskell Data.Vector.*
错过了C ++ std::vector::push_back
的功能。有grow
/ unsafeGrow
,但它们似乎有 O(n)复杂性。
有没有办法在元素的 O(1)摊销时间内增长向量?
答案 0 :(得分:11)
Data.Vector
中确实没有这样的设施。使用像Data.Vector.Mutable
这样的MutableArray
从头开始实现这一点并不困难(参见下面的实现),但是存在一些明显的缺点。特别是,它的所有操作最终都发生在一些状态上下文ST
或IO
内。这有缺点
vector
这样的库使用一些非常聪明的名为fusion来优化中间分配。在州情境中,这种事情是不可能的。ST
中我甚至不能拥有两个线程,在IO
中我会在整个地方都有竞争条件。这里令人讨厌的是,任何共享都必须在IO
。好像所有这些都不够,垃圾收集在纯代码中也表现得更好。
您不需要完全这种行为 - 通常您最好使用不可变数据结构(从而避免所有上述问题)做某事类似。仅限于GHC附带的containers
,一些替代方案包括:
push_back
,也许你只想要一个堆栈(一个普通的[a]
)。push_back
,Data.Sequence
会为您提供O(1)
附加到任意一端和O(log n)
查询。Data.IntMap
已经过优化。即使这些操作的理论成本为O(log n)
,您也需要一个非常大的IntMap
来开始感受这些成本。 vector
当然,如果一个人不关心最初提到的限制,就没有理由没有像C ++这样的向量。为了好玩,我继续从头开始实现这一点(需要包data-default
和primitive
)。
这个代码可能不在某个库中的原因是它违背了Haskell的大部分精神(我这样做是为了符合C ++样式向量)。
newVector
- 其他所有内容"修改"现有的矢量。由于pushBack
没有返回新的GrowVector
,因此必须修改现有的length
(包括其长度和/或容量),因此capacity
和length
具有成为"指针"。反过来,这意味着即使获得vector
也是一种单一操作。module GrowVector (
GrowVector, newEmpty, size, read, write, pushBack, popBack
) where
import Data.Primitive.Array
import Data.Primitive.MutVar
import Data.Default
import Control.Monad
import Control.Monad.Primitive (PrimState, PrimMonad)
import Prelude hiding (length, read)
data GrowVector s a = GrowVector
{ underlying :: MutVar s (MutableArray s a) -- ^ underlying array
, length :: MutVar s Int -- ^ perceived length of vector
, capacity :: MutVar s Int -- ^ actual capacity
}
type GrowVectorIO = GrowVector (PrimState IO)
-- | Make a new empty vector with the given capacity. O(n)
newEmpty :: (Default a, PrimMonad m) => Int -> m (GrowVector (PrimState m) a)
newEmpty cap = do
arr <- newArray cap def
GrowVector <$> newMutVar arr <*> newMutVar 0 <*> newMutVar cap
-- | Read an element in the vector (unchecked). O(1)
read :: PrimMonad m => GrowVector (PrimState m) a -> Int -> m a
g `read` i = do arr <- readMutVar (underlying g); arr `readArray` i
-- | Find the size of the vector. O(1)
size :: PrimMonad m => GrowVector (PrimState m) a -> m Int
size g = readMutVar (length g)
-- | Double the vector capacity. O(n)
resize :: (Default a, PrimMonad m) => GrowVector (PrimState m) a -> m ()
resize g = do
curCap <- readMutVar (capacity g) -- read current capacity
curArr <- readMutVar (underlying g) -- read current array
curLen <- readMutVar (length g) -- read current length
newArr <- newArray (2 * curCap) def -- allocate a new array twice as big
copyMutableArray newArr 1 curArr 1 curLen -- copy the old array over
underlying g `writeMutVar` newArr -- use the new array in the vector
capacity g `modifyMutVar'` (*2) -- update the capacity in the vector
-- | Write an element to the array (unchecked). O(1)
write :: PrimMonad m => GrowVector (PrimState m) a -> Int -> a -> m ()
write g i x = do arr <- readMutVar (underlying g); writeArray arr i x
-- | Pop an element of the vector, mutating it (unchecked). O(1)
popBack :: PrimMonad m => GrowVector (PrimState m) a -> m a
popBack g = do
s <- size g;
x <- g `read` (s - 1)
length g `modifyMutVar'` (+ negate 1)
pure x
-- | Push an element. (Amortized) O(1)
pushBack :: (Default a, PrimMonad m) => GrowVector (PrimState m) a -> a -> m ()
pushBack g x = do
s <- readMutVar (length g) -- read current size
c <- readMutVar (capacity g) -- read current capacity
when (s+1 == c) (resize g) -- if need be, resize
write g (s+1) x -- write to the back of the array
length g `modifyMutVar'` (+1) -- increase te length
s data family
approach并不太难 - 这只是单调乏味的 1 。随着说:
grow
grow
我认为github issue在解释语义方面做得非常好:
我认为预期的语义是它可以执行realloc,但不能保证,并且所有当前实现都执行更简单的复制语义,因为对于堆分配,成本应该大致相同。
基本上,当你想要一个增加大小的新的可变向量时,你应该使用GrowVector
,从旧向量的元素开始(不再关心旧向量)。这非常有用 - 例如,可以使用MVector
和grow
来实现data instance
。
1 方法是,对于您想要拥有的每种新类型的未装箱的矢量,您可以创建一个data family
来扩展&#34;您的类型为固定数量的未装箱数组(或其他未装箱的数据)。这是data instance
的要点 - 允许类型的不同实例化具有完全不同的运行时表示,并且也可以是可扩展的(如果需要,可以添加自己的import re
s = '{"a":"x","b":1,"c":"{"a":"x","b":1,"c":"{"a":"x","b":1,"c":"xa"}"}"}'
m = re.search(r'([^"]*"){9}(.*)"', s)
print(m.group(2))
。)