不,Data.Vector 中确实没有这样的设施。使用MutableArray 从头开始实现这一点并不难,就像Data.Vector.Mutable 所做的那样(见下面我的实现),但有一些明显的缺点。特别是,它的所有操作最终都发生在某个状态上下文中,通常是ST 或IO。这样做的缺点是
- 任何操作这种数据结构的代码最终都必须是单子的
- 编译器不太可能进行优化。例如,像
vector 这样的库使用称为fusion 的非常聪明的东西来优化中间分配。这种事情在状态上下文中是不可能的。
- 并行性将变得更加困难:在
ST 中我什至不能有两个线程,而在IO 中我将到处都有竞争条件。这里令人讨厌的一点是,任何共享都必须在IO 中进行。
似乎这一切还不够,垃圾收集在纯代码中也表现得更好。
那我该怎么办?
您并不经常需要正是这种行为 - 通常您最好使用不可变的数据结构(从而避免所有上述问题),它会做类似的事情。仅限于 GHC 附带的containers,一些替代方案包括:
- 如果您几乎总是只使用
push_back,也许您只需要一个堆栈(一个普通的旧[a])。
- 如果您预计
push_back 比查找更多,Data.Sequence 会为您提供O(1) 附加到任一端和O(log n) 查找。
- 如果您对很多操作感兴趣,尤其是类似 hashmap 的操作,
Data.IntMap 已经非常优化。即使这些操作的理论成本是O(log n),您也需要相当大的IntMap 才能开始感受这些成本。
制作类似 C++ 的东西 vector
当然,如果你不关心最初提到的限制,那么没有理由不使用类似 C++ 的向量。只是为了好玩,我从头开始实现了这个(需要包 data-default 和 primitive)。
此代码可能不在某些库中的原因是它违背了 Haskell 的大部分精神(我这样做是为了符合 C++ 样式向量)。
- 真正创建新向量的唯一操作是
newVector - 其他所有操作都会“修改”现有向量。由于pushBack 不会返回一个新的GrowVector,它必须修改现有的(包括它的长度和/或容量),所以length 和capacity 必须是“指针”。反过来,这意味着即使获取 length 也是一个单子操作。
- 虽然没有拆箱,但复制
vectors data family approach 并不会太难 - 只是很乏味1。
话虽如此:
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
grow 的当前语义
我认为github issue 很好地解释了语义:
我认为预期的语义是它可能会执行重新分配,但不能保证这样做,并且所有当前的实现都执行更简单的复制语义,因为对于堆分配,成本应该大致相同。
基本上你应该使用grow,当你想要一个更大的新可变向量时,从旧向量的元素开始(不再关心旧向量)。这非常有用——例如,可以使用MVector 和grow 实现GrowVector。
1 方法是,对于您想要拥有的每种新类型的未装箱矢量,您创建一个 data instance 将您的类型“扩展”为固定数量的未装箱数组(或其他未装箱的数组)向量)。这就是data family 的重点——允许一个类型的不同实例具有完全不同的运行时表示,并且还可以扩展(如果需要,您可以添加自己的data instance)。