diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,36 @@
+# 0.2.2.1 - July 2024
+
+* Add `sort`, `sortBy`, `sortOn` ([#23](https://github.com/konsumlamm/rrb-vector/pull/22))
+* Fix bug in `><` ([#19](https://github.com/konsumlamm/rrb-vector/pull/19))
+* Fix bug in `<|` ([#18](https://github.com/konsumlamm/rrb-vector/pull/18))
+
+# 0.2.1.0 - December 2023
+
+* Add `findIndexL`, `findIndexR`, `findIndicesL`, `findIndicesR`
+* Fix bug in `|>` & `<|` ([#10](https://github.com/konsumlamm/rrb-vector/issues/10))
+
+# 0.2.0.1 - October 2023
+
+* Support `primitive-0.9` and `deepseq-1.5`
+
+# 0.2.0.0 - February 2023
+
+* Add `map'` & `unzipWith`
+* Make `zip` & `unzip` more strict
+* Make all functions strict in the index
+* Make the `FoldableWithIndex`, `FunctorWithIndex`, `TraversableWithIndex` instances more efficient
+* Implement `ifoldMap` in the `FoldableWithIndex` instance
+* Implement `fail` in the `Monad` instance
+* Add `Shift`, `Tree` and unidirectional pattern synonyms for `Vector` and `Tree` to `Data.RRBVector.Internal.Debug`
+* Support `primitive-0.8`
+
+* Test typeclass laws
+
 # 0.1.1.0 - August 2021
 
 * Add `replicate`
 * Various optimizations
+
 * Switch to the [`tasty` framework](https://hackage.haskell.org/package/tasty) for benchmarks and tests
 * Extend the test suite
   - Add properties for more functions and some typeclass instances
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,2 +0,0 @@
-import Distribution.Simple
-main = defaultMain
diff --git a/bench/Main.hs b/bench/Main.hs
--- a/bench/Main.hs
+++ b/bench/Main.hs
@@ -6,19 +6,29 @@
 
 import qualified Data.RRBVector as RRB
 
+default (Int)
+
 main :: IO ()
 main = defaultMain $ [10, 100, 1000, 10000, 100000] <&> \n ->
     let !v = RRB.fromList [1..n]
         !idx = n `div` 2
     in bgroup (show n)
-    [ bench "fromList" $ nf RRB.fromList [1..n]
+    [ bench "fromList" $ whnf RRB.fromList [1..n]
     , bench "><" $ whnf (\vec -> vec RRB.>< vec) v
     , bench "|>" $ whnf (RRB.|> 42) v
     , bench "<|" $ whnf (42 RRB.<|) v
     , bench "take" $ whnf (RRB.take idx) v
     , bench "drop" $ whnf (RRB.drop idx) v
+    , bench "splitAt" $ whnf (RRB.splitAt idx) v
+    , bench "insertAt" $ whnf (RRB.insertAt idx 42) v
+    , bench "deleteAt" $ whnf (RRB.deleteAt idx) v
     , bench "index" $ nf (RRB.lookup idx) v
     , bench "adjust" $ whnf (RRB.adjust idx (+ 1)) v
+    , bench "map" $ whnf (RRB.map (+ 1)) v
     , bench "foldl" $ nf (foldl (+) 0) v
     , bench "foldr" $ nf (foldr (+) 0) v
+    , bench "findIndexL" $ nf (RRB.findIndexL (== idx)) v
+    , bench "findIndexR" $ nf (RRB.findIndexR (== idx)) v
+    , bench "findIndicesL" $ nf (RRB.findIndicesL (== idx)) v
+    , bench "findIndicesR" $ nf (RRB.findIndicesR (== idx)) v
     ]
diff --git a/bench/Traverse.hs b/bench/Traverse.hs
deleted file mode 100644
--- a/bench/Traverse.hs
+++ /dev/null
@@ -1,25 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
-import Control.Applicative (liftA2)
-import Control.Monad (replicateM)
-import Data.Foldable (foldl', toList)
-import Data.Functor
-
-import Test.Tasty.Bench
-
-import qualified Data.RRBVector as RRB
-
-main :: IO ()
-main = defaultMain $ [100, 1000, 10000] <&> \n ->
-    let !v = RRB.fromList [1..n]
-        !idx = n `div` 2
-    in bgroup (show n)
-        [ bench "foldr" $ nf (foldr (+) 0) v
-        , bench "ifoldr" $ nf (RRB.ifoldr (\i x acc -> i + x + acc) 0) v
-        , bench "foldl" $ nf (foldl (+) 0) v
-        , bench "ifoldl" $ nf (RRB.ifoldl (\i acc x -> i + acc + x) 0) v
-        , bench "map" $ nf (RRB.map (+ 1)) v
-        , bench "imap" $ nf (RRB.imap (+)) v
-        , bench "traverse" $ nf (traverse (Just $!)) v
-        , bench "itraverse" $ nf (RRB.itraverse (\i x -> Just $! i + x)) v
-        ]
diff --git a/rrb-vector.cabal b/rrb-vector.cabal
--- a/rrb-vector.cabal
+++ b/rrb-vector.cabal
@@ -1,5 +1,5 @@
 name:               rrb-vector
-version:            0.1.1.0
+version:            0.2.2.1
 synopsis:           Efficient RRB-Vectors
 description:
   An RRB-Vector is an efficient sequence data structure.
@@ -22,11 +22,21 @@
   CHANGELOG.md
   README.md
 cabal-version:      2.0
-tested-with:        GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.5, GHC == 9.0.1
+tested-with:
+  GHC == 8.4.4
+  GHC == 8.6.5
+  GHC == 8.8.4
+  GHC == 8.10.7
+  GHC == 9.0.2
+  GHC == 9.2.8
+  GHC == 9.4.8
+  GHC == 9.6.5
+  GHC == 9.8.2
+  GHC == 9.10.1
 
 source-repository head
   type:     git
-  location: https://github.com/konsumlamm/rrb-vector
+  location: https://github.com/konsumlamm/rrb-vector.git
 
 library
   hs-source-dirs:       src
@@ -37,10 +47,15 @@
     Data.RRBVector.Internal
     Data.RRBVector.Internal.Array
     Data.RRBVector.Internal.Buffer
-    Data.RRBVector.Internal.Indexed
     Data.RRBVector.Internal.IntRef
-  build-depends:        base >= 4.11 && < 5, deepseq ^>= 1.4.3, indexed-traversable ^>= 0.1, primitive ^>= 0.7
-  ghc-options:          -O2 -Wall -Wno-name-shadowing
+    Data.RRBVector.Internal.Sorting
+  build-depends:
+    base >= 4.11 && < 5,
+    deepseq >= 1.4.3 && < 1.6,
+    indexed-traversable ^>= 0.1,
+    primitive >= 0.7.3 && < 0.10,
+    samsort ^>= 0.1
+  ghc-options:          -O2 -Wall -Wno-name-shadowing -Werror=missing-methods -Werror=missing-fields
   default-language:     Haskell2010
 
 test-suite test
@@ -52,9 +67,9 @@
     Strictness
   type:                 exitcode-stdio-1.0
   ghc-options:          -Wall -Wno-orphans -Wno-type-defaults
-  build-depends:        base >= 4.11 && < 5, deepseq, indexed-traversable, rrb-vector, tasty, tasty-quickcheck
-  if impl(ghc == 8.10.*)
-    build-depends:      ghc-heap-view
+  build-depends:        base, containers, deepseq, quickcheck-classes-base, rrb-vector, tasty, tasty-quickcheck
+  if impl(ghc >= 8.6)
+    build-depends:      nothunks
   default-language:     Haskell2010
   default-extensions:   ExtendedDefaultRules, ScopedTypeVariables
 
@@ -64,14 +79,5 @@
   type:                 exitcode-stdio-1.0
   default-language:     Haskell2010
   ghc-options:          -O2
-  build-depends:        base >= 4.11 && < 5, rrb-vector, tasty-bench
-  default-extensions:   ExtendedDefaultRules
-
-benchmark traverse
-  hs-source-dirs:       bench
-  main-is:              Traverse.hs
-  type:                 exitcode-stdio-1.0
-  default-language:     Haskell2010
-  ghc-options:          -O2
-  build-depends:        base >= 4.11 && < 5, primitive, rrb-vector, tasty-bench
+  build-depends:        base, rrb-vector, tasty-bench
   default-extensions:   ExtendedDefaultRules
diff --git a/src/Data/RRBVector.hs b/src/Data/RRBVector.hs
--- a/src/Data/RRBVector.hs
+++ b/src/Data/RRBVector.hs
@@ -33,6 +33,7 @@
     , adjust, adjust'
     , take, drop, splitAt
     , insertAt, deleteAt
+    , findIndexL, findIndexR, findIndicesL, findIndicesR
     -- * With Index
     --
     -- | Reexported from [indexed-traversable](https://hackage.haskell.org/package/indexed-traversable).
@@ -40,9 +41,13 @@
     , module Data.Functor.WithIndex
     , module Data.Traversable.WithIndex
     -- * Transformations
-    , map, reverse
+    , map, map', reverse
     -- * Zipping and unzipping
-    , zip, zipWith, unzip
+    , zip, zipWith, unzip, unzipWith
+    -- * Sorting
+    --
+    -- | Currently implemented using [samsort](https://hackage.haskell.org/package/samsort).
+    , sort, sortBy, sortOn
     ) where
 
 import Prelude hiding (replicate, lookup, take, drop, splitAt, map, reverse, zip, zipWith, unzip)
@@ -52,3 +57,4 @@
 import Data.Traversable.WithIndex
 
 import Data.RRBVector.Internal
+import Data.RRBVector.Internal.Sorting
diff --git a/src/Data/RRBVector/Internal.hs b/src/Data/RRBVector/Internal.hs
--- a/src/Data/RRBVector/Internal.hs
+++ b/src/Data/RRBVector/Internal.hs
@@ -3,12 +3,14 @@
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
 
 module Data.RRBVector.Internal
     ( Vector(..)
     , Tree(..)
+    , Shift
     -- * Internal
-    , blockShift, blockSize, treeSize, computeSizes, up
+    , blockShift, blockSize, treeSize, computeSizes, up, down
     -- * Construction
     , empty, singleton, fromList, replicate
     -- ** Concatenation
@@ -22,30 +24,34 @@
     , adjust, adjust'
     , take, drop, splitAt
     , insertAt, deleteAt
+    , findIndexL, findIndexR, findIndicesL, findIndicesR
     -- * Transformations
-    , map, reverse
+    , map, map', reverse
     -- * Zipping and unzipping
-    , zip, zipWith, unzip
+    , zip, zipWith, unzip, unzipWith
     ) where
 
+#if !(MIN_VERSION_base(4,18,0))
 import Control.Applicative (Alternative, liftA2)
+#else
+import Control.Applicative (Alternative)
+#endif
 import qualified Control.Applicative
 import Control.DeepSeq
 import Control.Monad (when, MonadPlus)
 import Control.Monad.ST (runST)
-#if !(MIN_VERSION_base(4,13,0))
-import Control.Monad.Fail (MonadFail(..))
-#endif
+import qualified Control.Monad.Fail as Fail
 import Control.Monad.Fix (MonadFix(..))
 import Control.Monad.Zip (MonadZip(..))
 
 import Data.Bits
 import Data.Foldable (Foldable(..), for_)
 import Data.Functor.Classes
-import Data.Functor.Identity (Identity(..))
 import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty(..))
 import Data.Maybe (fromMaybe)
 import Data.Semigroup
+
 import qualified GHC.Exts as Exts
 import GHC.Stack (HasCallStack)
 import Prelude hiding (replicate, lookup, map, take, drop, splitAt, head, last, reverse, zip, zipWith, unzip)
@@ -55,10 +61,9 @@
 import Data.Foldable.WithIndex
 import Data.Traversable.WithIndex
 
-import Data.Primitive.PrimArray
+import Data.Primitive.PrimArray hiding (sizeofPrimArray) -- use @length@ of the @A.Array@ instead
 import qualified Data.RRBVector.Internal.Array as A
 import qualified Data.RRBVector.Internal.Buffer as Buffer
-import Data.RRBVector.Internal.Indexed
 
 infixr 5 ><
 infixr 5 <|
@@ -66,9 +71,10 @@
 
 type Shift = Int
 
--- Invariant: Children of a Balanced node are always balanced.
--- A Leaf node is considered balanced.
+-- Invariants:
+-- Children of a Balanced node are always balanced (a Leaf node is considered balanced).
 -- Nodes are always non-empty.
+-- The two arrays in an Unbalanced node always have the same size.
 data Tree a
     = Balanced {-# UNPACK #-} !(A.Array (Tree a))
     | Unbalanced {-# UNPACK #-} !(A.Array (Tree a)) !(PrimArray Int)
@@ -131,7 +137,7 @@
 treeSize = go 0
   where
     go !acc !_ (Leaf arr) = acc + length arr
-    go acc _ (Unbalanced _ sizes) = acc + indexPrimArray sizes (sizeofPrimArray sizes - 1)
+    go acc _ (Unbalanced arr sizes) = acc + indexPrimArray sizes (length arr - 1)
     go acc sh (Balanced arr) =
         let i = length arr - 1
         in go (acc + i * (1 `unsafeShiftL` sh)) (down sh) (A.index arr i)
@@ -143,7 +149,7 @@
 computeSizes !sh arr
     | isBalanced = Balanced arr
     | otherwise = runST $ do
-        sizes <- newPrimArray (length arr)
+        sizes <- newPrimArray len
         let loop acc i
                 | i < len =
                     let size = treeSize (down sh) (A.index arr i)
@@ -168,7 +174,7 @@
           where
             subtree = A.index arr i
 
--- Integer log base 2.
+-- | Integer log base 2.
 log2 :: Int -> Int
 log2 x = bitSizeMinus1 - countLeadingZeros x
   where
@@ -189,6 +195,9 @@
     readListPrec = readListPrecDefault
 
 instance Eq1 Vector where
+    liftEq _ Empty Empty = True
+    liftEq _ Empty _ = False
+    liftEq _ _ Empty = False
     liftEq f v1 v2 = length v1 == length v2 && liftEq f (toList v1) (toList v2)
 
 instance (Eq a) => Eq (Vector a) where
@@ -202,10 +211,12 @@
 
 instance Semigroup (Vector a) where
     (<>) = (><)
+    sconcat (v :| vs) = v >< mconcat vs
     stimes = stimesMonoid
 
 instance Monoid (Vector a) where
     mempty = empty
+    mconcat = foldl' (><) empty
 
 instance Foldable Vector where
     foldr f acc = go
@@ -255,20 +266,60 @@
     length (Root s _ _) = s
 
 instance FoldableWithIndex Int Vector where
-    ifoldr f z0 v = foldr' (\x g !i -> f i x (g (i + 1))) (const z0) v 0
+    ifoldMap f = ifoldr (\i x acc -> f i x <> acc) mempty
+    {-# INLINE ifoldMap #-}
+
+    ifoldr f acc = go
+      where
+        go Empty = acc
+        go (Root _ sh tree) = ifoldrTree 0 sh tree acc
+
+        ifoldrTree !i !sh (Balanced arr) acc' = A.ifoldrStep i (treeSize (down sh)) (flip ifoldrTree (down sh)) acc' arr
+        ifoldrTree i sh (Unbalanced arr _) acc' = A.ifoldrStep i (treeSize (down sh)) (flip ifoldrTree (down sh)) acc' arr
+        ifoldrTree i _ (Leaf arr) acc' = A.ifoldrStep i (\_ -> 1) f acc' arr
     {-# INLINE ifoldr #-}
 
-    ifoldl f z0 v = foldl' (\g x !i -> f i (g (i - 1)) x) (const z0) v (length v - 1)
+    ifoldl f acc = go
+      where
+        go Empty = acc
+        go (Root size sh tree) = ifoldlTree (size - 1) sh acc tree
+
+        ifoldlTree !i !sh acc' (Balanced arr) = A.ifoldlStep i (treeSize (down sh)) (flip ifoldlTree (down sh)) acc' arr
+        ifoldlTree i sh acc' (Unbalanced arr _) = A.ifoldlStep i (treeSize (down sh)) (flip ifoldlTree (down sh)) acc' arr
+        ifoldlTree i _ acc' (Leaf arr) = A.ifoldlStep i (\_ -> 1) f acc' arr
     {-# INLINE ifoldl #-}
 
+    ifoldr' f acc = go
+      where
+        go Empty = acc
+        go (Root size sh tree) = ifoldrTree' (size - 1) sh tree acc
+
+        ifoldrTree' !i !sh (Balanced arr) acc' = A.ifoldrStep' i (treeSize (down sh)) (flip ifoldrTree' (down sh)) acc' arr
+        ifoldrTree' i sh (Unbalanced arr _) acc' = A.ifoldrStep' i (treeSize (down sh)) (flip ifoldrTree' (down sh)) acc' arr
+        ifoldrTree' i _ (Leaf arr) acc' = A.ifoldrStep' i (\_ -> 1) f acc' arr
+    {-# INLINE ifoldr' #-}
+
+    ifoldl' f acc = go
+      where
+        go Empty = acc
+        go (Root _ sh tree) = ifoldlTree' 0 sh acc tree
+
+        ifoldlTree' !i !sh acc' (Balanced arr) = A.ifoldlStep' i (treeSize (down sh)) (flip ifoldlTree' (down sh)) acc' arr
+        ifoldlTree' i sh acc' (Unbalanced arr _) = A.ifoldlStep' i (treeSize (down sh)) (flip ifoldlTree' (down sh)) acc' arr
+        ifoldlTree' i _ acc' (Leaf arr) = A.ifoldlStep' i (\_ -> 1) f acc' arr
+    {-# INLINE ifoldl' #-}
+
 instance Functor Vector where
     fmap = map
     x <$ v = replicate (length v) x
 
 instance FunctorWithIndex Int Vector where
-    imap f v = runIdentity $ evalIndexed (traverse (Indexed . f') v) 0
+    imap _ Empty = Empty
+    imap f (Root size sh tree) = Root size sh (imapTree 0 sh tree)
       where
-        f' x !i = WithIndex (i + 1) (Identity (f i x))
+        imapTree !i !sh (Balanced arr) = Balanced (A.imapStep' i (treeSize (down sh)) (\i -> imapTree i (down sh)) arr)
+        imapTree i sh (Unbalanced arr sizes) = Unbalanced (A.imapStep' i (treeSize (down sh)) (\i -> imapTree i (down sh)) arr) sizes
+        imapTree i _ (Leaf arr) = Leaf (A.imapStep i (\_ -> 1) f arr)
 
 instance Traversable Vector where
     traverse _ Empty = pure Empty
@@ -280,20 +331,27 @@
     {-# INLINE traverse #-}
 
 instance TraversableWithIndex Int Vector where
-    itraverse f v = evalIndexed (traverse (Indexed . f') v) 0
+    itraverse _ Empty = pure Empty
+    itraverse f (Root size sh tree) = Root size sh <$> itraverseTree 0 sh tree
       where
-        f' x !i = WithIndex (i + 1) (f i x)
+        itraverseTree !i !sh (Balanced arr) = Balanced <$> A.itraverseStep' i (treeSize (down sh)) (\i -> itraverseTree i (down sh)) arr
+        itraverseTree i sh (Unbalanced arr sizes) = flip Unbalanced sizes <$> A.itraverseStep' i (treeSize (down sh)) (\i -> itraverseTree i (down sh)) arr
+        itraverseTree i _ (Leaf arr) = Leaf <$> A.itraverseStep i (\_ -> 1) f arr
     {-# INLINE itraverse #-}
 
 instance Applicative Vector where
     pure = singleton
     fs <*> xs = foldl' (\acc f -> acc >< map f xs) empty fs
     liftA2 f xs ys = foldl' (\acc x -> acc >< map (f x) ys) empty xs
-    xs *> ys = foldl' (\acc _ -> acc >< ys) empty xs
+    xs *> ys = stimes (length xs) ys
     xs <* ys = foldl' (\acc x -> acc >< replicate (length ys) x) empty xs
 
 instance Monad Vector where
     xs >>= f = foldl' (\acc x -> acc >< f x) empty xs
+    (>>) = (*>)
+#if !(MIN_VERSION_base(4,13,0))
+    fail = Fail.fail
+#endif
 
 instance Alternative Vector where
     empty = empty
@@ -301,11 +359,13 @@
 
 instance MonadPlus Vector
 
-instance MonadFail Vector where
+instance Fail.MonadFail Vector where
     fail _ = empty
 
 instance MonadFix Vector where
-    mfix f = fromList $ fmap (\i -> let x = index i (f x) in x) [0..length (f err) - 1]
+    mfix f = case f err of
+        Empty -> Empty
+        Root size _ _ -> fromList $ fmap (\i -> let x = f x ! i in x) [0..size - 1]
       where
         err = error "mfix for Data.RRBVector.Vector applied to strict function"
 
@@ -352,16 +412,14 @@
         buffer <- Buffer.new blockSize
         let loop [] = do
                 result <- Buffer.get buffer
-                let !x = f result
-                pure [x]
+                pure [f result]
             loop (t : ts) = do
                 size <- Buffer.size buffer
                 if size == blockSize then do
                     result <- Buffer.get buffer
                     Buffer.push buffer t
                     rest <- loop ts
-                    let !x = f result
-                    pure (x : rest)
+                    pure (f result : rest)
                 else do
                     Buffer.push buffer t
                     loop ts
@@ -395,7 +453,7 @@
 
 -- | \(O(\log n)\). The element at the index or 'Nothing' if the index is out of range.
 lookup :: Int -> Vector a -> Maybe a
-lookup _ Empty = Nothing
+lookup !_ Empty = Nothing
 lookup i (Root size sh tree)
     | i < 0 || i >= size = Nothing  -- index out of range
     | otherwise = Just $ lookupTree i sh tree
@@ -405,38 +463,41 @@
         let (idx, subIdx) = relaxedRadixIndex sizes i sh
         in lookupTree subIdx (down sh) (A.index arr idx)
     lookupTree i _ (Leaf arr) = A.index arr (i .&. blockMask)
+{-# INLINE lookup #-}
 
 -- | \(O(\log n)\). The element at the index. Calls 'error' if the index is out of range.
 index :: HasCallStack => Int -> Vector a -> a
-index i = fromMaybe (error "AMT.index: index out of range") . lookup i
+index i = fromMaybe (error "index out of range") . lookup i
 {-# INLINE index #-}
 
 -- | \(O(\log n)\). A flipped version of 'lookup'.
 (!?) :: Vector a -> Int -> Maybe a
 (!?) = flip lookup
+{-# INLINE (!?) #-}
 
 -- | \(O(\log n)\). A flipped version of 'index'.
 (!) :: HasCallStack => Vector a -> Int -> a
 (!) = flip index
+{-# INLINE (!) #-}
 
 -- | \(O(\log n)\). Update the element at the index with a new element.
 -- If the index is out of range, the original vector is returned.
 update :: Int -> a -> Vector a -> Vector a
-update _ _ Empty = Empty
+update !_ _ Empty = Empty
 update i x v@(Root size sh tree)
     | i < 0 || i >= size = v  -- index out of range
-    | otherwise = Root size sh (adjustTree i sh tree)
+    | otherwise = Root size sh (updateTree i sh tree)
   where
-    adjustTree i sh (Balanced arr) = Balanced (A.adjust' arr (radixIndex i sh) (adjustTree i (down sh)))
-    adjustTree i sh (Unbalanced arr sizes) =
+    updateTree i sh (Balanced arr) = Balanced (A.adjust' arr (radixIndex i sh) (updateTree i (down sh)))
+    updateTree i sh (Unbalanced arr sizes) =
         let (idx, subIdx) = relaxedRadixIndex sizes i sh
-        in Unbalanced (A.adjust' arr idx (adjustTree subIdx (down sh))) sizes
-    adjustTree i _ (Leaf arr) = Leaf (A.update arr (i .&. blockMask) x)
+        in Unbalanced (A.adjust' arr idx (updateTree subIdx (down sh))) sizes
+    updateTree i _ (Leaf arr) = Leaf (A.update arr (i .&. blockMask) x)
 
 -- | \(O(\log n)\). Adjust the element at the index by applying the function to it.
 -- If the index is out of range, the original vector is returned.
 adjust :: Int -> (a -> a) -> Vector a -> Vector a
-adjust _ _ Empty = Empty
+adjust !_ _ Empty = Empty
 adjust i f v@(Root size sh tree)
     | i < 0 || i >= size = v  -- index out of range
     | otherwise = Root size sh (adjustTree i sh tree)
@@ -449,7 +510,7 @@
 
 -- | \(O(\log n)\). Like 'adjust', but the result of the function is forced.
 adjust' :: Int -> (a -> a) -> Vector a -> Vector a
-adjust' _ _ Empty = Empty
+adjust' !_ _ Empty = Empty
 adjust' i f v@(Root size sh tree)
     | i < 0 || i >= size = v  -- index out of range
     | otherwise = Root size sh (adjustTree i sh tree)
@@ -472,19 +533,30 @@
     mapTree (Unbalanced arr sizes) = Unbalanced (A.map' mapTree arr) sizes
     mapTree (Leaf arr) = Leaf (A.map f arr)
 
+-- | \(O(n)\). Like 'map', but the results of the function are forced.
+--
+-- @since 0.2.0.0
+map' :: (a -> b) -> Vector a -> Vector b
+map' _ Empty = Empty
+map' f (Root size sh tree) = Root size sh (mapTree tree)
+  where
+    mapTree (Balanced arr) = Balanced (A.map' mapTree arr)
+    mapTree (Unbalanced arr sizes) = Unbalanced (A.map' mapTree arr) sizes
+    mapTree (Leaf arr) = Leaf (A.map' f arr)
+
 -- | \(O(n)\). Reverse the vector.
 --
 -- >>> reverse (fromList [1, 2, 3])
 -- fromList [3,2,1]
 reverse :: Vector a -> Vector a
-reverse = fromList . foldl' (flip (:)) [] -- convert the vector to a reverse list and then rebuild
+reverse v
+    | length v <= 1 = v
+    | otherwise = fromList (foldl' (flip (:)) [] v) -- convert the vector to a reverse list and then rebuild
 
 -- | \(O(\min(n_1, n_2))\). Take two vectors and return a vector of corresponding pairs.
 -- If one input is longer, excess elements are discarded from the right end.
---
--- > zip = zipWith (,)
 zip :: Vector a -> Vector b -> Vector (a, b)
-zip = zipWith (,)
+zip v1 v2 = fromList $ List.zip (toList v1) (toList v2)
 
 -- | \(O(\min(n_1, n_2))\). 'zipWith' generalizes 'zip' by zipping with the function.
 zipWith :: (a -> b -> c) -> Vector a -> Vector b -> Vector c
@@ -495,11 +567,25 @@
 -- >>> unzip (fromList [(1, "a"), (2, "b"), (3, "c")])
 -- (fromList [1,2,3],fromList ["a","b","c"])
 unzip :: Vector (a, b) -> (Vector a, Vector b)
-unzip v =
-    let !left = map fst v
-        !right = map snd v
-    in (left, right)
+unzip = Exts.inline unzipWith id
 
+-- | \(O(n)\). Unzip a vector with a function.
+--
+-- > unzipWith f = unzip . map f
+--
+-- @since 0.2.0.0
+unzipWith :: (a -> (b, c)) -> Vector a -> (Vector b, Vector c)
+unzipWith _ Empty = (Empty, Empty)
+unzipWith f (Root size sh tree) = case unzipTree tree of
+    (!left, !right) -> (Root size sh left, Root size sh right)
+  where
+    unzipTree (Balanced arr) = case A.unzipWith unzipTree arr of
+        (!left, !right) -> (Balanced left, Balanced right)
+    unzipTree (Unbalanced arr sizes) = case A.unzipWith unzipTree arr of
+        (!left, !right) -> (Unbalanced left sizes, Unbalanced right sizes)
+    unzipTree (Leaf arr) = case A.unzipWith f arr of
+        (!left, !right) -> (Leaf left, Leaf right)
+
 -- | \(O(\log n)\). The first element and the vector without the first element, or 'Nothing' if the vector is empty.
 --
 -- >>> viewl (fromList [1, 2, 3])
@@ -524,23 +610,76 @@
     lastTree (Unbalanced arr _) = lastTree (A.last arr)
     lastTree (Leaf arr) = A.last arr
 
+-- | \(O(\log n)\). The first @i@ elements of the vector.
+-- If @i@ is negative, the empty vector is returned. If the vector contains less than @i@ elements, the whole vector is returned.
+take :: Int -> Vector a -> Vector a
+take !_ Empty = Empty
+take n v@(Root size sh tree)
+    | n <= 0 = empty
+    | n >= size = v
+    | otherwise = normalize $ Root n sh (takeTree (n - 1) sh tree)
+
+-- | \(O(\log n)\). The vector without the first @i@ elements.
+-- If @i@ is negative, the whole vector is returned. If the vector contains less than @i@ elements, the empty vector is returned.
+drop :: Int -> Vector a -> Vector a
+drop !_ Empty = Empty
+drop n v@(Root size sh tree)
+    | n <= 0 = v
+    | n >= size = empty
+    | otherwise = normalize $ Root (size - n) sh (dropTree n sh tree)
+
 -- | \(O(\log n)\). Split the vector at the given index.
 --
 -- > splitAt n v = (take n v, drop n v)
 splitAt :: Int -> Vector a -> (Vector a, Vector a)
-splitAt n v =
-    let !left = take n v
-        !right = drop n v
-    in (left, right)
+splitAt !_ Empty = (Empty, Empty)
+splitAt n v@(Root size sh tree)
+    | n <= 0 = (empty, v)
+    | n >= size = (v, empty)
+    | otherwise =
+        let !left = normalize $ Root n sh (takeTree (n - 1) sh tree)
+            !right = normalize $ Root (size - n) sh (dropTree n sh tree)
+        in (left, right)
 
--- | \(O(\log n)\). Insert an element at the given index.
+-- | \(O(\log n)\). Insert an element at the given index, shifting the rest of the vector over.
+-- If the index is negative, add the element to the left end of the vector.
+-- If the index is bigger than or equal to the length of the vector, add the element to the right end of the vector.
 insertAt :: Int -> a -> Vector a -> Vector a
 insertAt i x v = let (left, right) = splitAt i v in (left |> x) >< right
 
 -- | \(O(\log n)\). Delete the element at the given index.
+-- If the index is out of range, return the original vector.
 deleteAt :: Int -> Vector a -> Vector a
 deleteAt i v = let (left, right) = splitAt (i + 1) v in take i left >< right
 
+-- | \(O(n)\). Find the first index from the left that satisfies the predicate.
+--
+-- @since 0.2.1.0
+findIndexL :: (a -> Bool) -> Vector a -> Maybe Int
+findIndexL f = ifoldr (\i x acc -> if f x then Just i else acc) Nothing
+{-# INLINE findIndexL #-}
+
+-- | \(O(n)\). Find the first index from the right that satisfies the predicate.
+--
+-- @since 0.2.1.0
+findIndexR :: (a -> Bool) -> Vector a -> Maybe Int
+findIndexR f = ifoldl (\i acc x -> if f x then Just i else acc) Nothing
+{-# INLINE findIndexR #-}
+
+-- | \(O(n)\). Find the indices that satisfy the predicate, starting from the left.
+--
+-- @since 0.2.1.0
+findIndicesL :: (a -> Bool) -> Vector a -> [Int]
+findIndicesL f = ifoldr (\i x acc -> if f x then i : acc else acc) []
+{-# INLINE findIndicesL #-}
+
+-- | \(O(n)\). Find the indices that satisfy the predicate, starting from the right.
+--
+-- @since 0.2.1.0
+findIndicesR :: (a -> Bool) -> Vector a -> [Int]
+findIndicesR f = ifoldl (\i acc x -> if f x then i : acc else acc) []
+{-# INLINE findIndicesR #-}
+
 -- concatenation
 
 -- | \(O(\log \max(n_1, n_2))\). Concatenates two vectors.
@@ -551,18 +690,15 @@
 Empty >< v = v
 v >< Empty = v
 Root size1 sh1 tree1 >< Root size2 sh2 tree2 =
-    let maxShift = max sh1 sh2
-        upMaxShift = up maxShift
+    let upMaxShift = up (max sh1 sh2)
         newArr = mergeTrees tree1 sh1 tree2 sh2
-    in if length newArr == 1
-        then Root (size1 + size2) maxShift (A.head newArr)
-        else Root (size1 + size2) upMaxShift (computeSizes upMaxShift newArr)
+    in normalize $ Root (size1 + size2) upMaxShift (computeSizes upMaxShift newArr)
   where
     mergeTrees tree1@(Leaf arr1) !_ tree2@(Leaf arr2) !_
         | length arr1 == blockSize = A.from2 tree1 tree2
-        | length arr1 + length arr2 <= blockSize = A.singleton $! Leaf (arr1 <> arr2)
+        | length arr1 + length arr2 <= blockSize = A.singleton $! Leaf (arr1 A.++ arr2)
         | otherwise =
-            let (left, right) = A.splitAt (arr1 <> arr2) blockSize
+            let (left, right) = A.splitAt (arr1 A.++ arr2) blockSize -- 'A.splitAt' doesn't copy anything
                 !leftTree = Leaf left
                 !rightTree = Leaf right
             in A.from2 leftTree rightTree
@@ -641,7 +777,10 @@
     -- compute the shift at which the new branch needs to be inserted (0 means there is space in the leaf)
     -- the size is computed for efficient calculation of the shift in a balanced subtree
     computeShift !sz !sh !min (Balanced _) =
-        let newShift = (log2 sz `div` blockShift) * blockShift
+        -- @sz - 1@ is the index of the last element
+        let hiShift = max ((log2 (sz - 1) `div` blockShift) * blockShift) 0 -- the shift of the root when normalizing
+            hi = (sz - 1) `unsafeShiftR` hiShift -- the length of the root node when normalizing minus 1
+            newShift = if hi < blockMask then hiShift else hiShift + blockShift
         in if newShift > sh then min else newShift
     computeShift _ sh min (Unbalanced arr sizes) =
         let sz' = indexPrimArray sizes 0 -- the size of the first subtree
@@ -666,21 +805,20 @@
         | sh == insertShift = Unbalanced (A.snoc arr $! newBranch x (down sh)) newSizesSnoc
         | otherwise = Unbalanced (A.adjust' arr (length arr - 1) (snocTree (down sh))) newSizesAdjust
       where
+        len = length arr
         -- snoc the last size + 1
         newSizesSnoc = runST $ do
-            let lenSizes = sizeofPrimArray sizes
-            newArr <- newPrimArray (lenSizes + 1)
-            copyPrimArray newArr 0 sizes 0 lenSizes
-            let lastSize = indexPrimArray sizes (lenSizes - 1)
-            writePrimArray newArr lenSizes (lastSize + 1)
+            newArr <- newPrimArray (len + 1)
+            copyPrimArray newArr 0 sizes 0 len
+            let lastSize = indexPrimArray sizes (len - 1)
+            writePrimArray newArr len (lastSize + 1)
             unsafeFreezePrimArray newArr
         -- adjust the last size with (+ 1)
         newSizesAdjust = runST $ do
-            let lenSizes = sizeofPrimArray sizes
-            newArr <- newPrimArray lenSizes
-            copyPrimArray newArr 0 sizes 0 lenSizes
-            let lastSize = indexPrimArray sizes (lenSizes - 1)
-            writePrimArray newArr (lenSizes - 1) (lastSize + 1)
+            newArr <- newPrimArray len
+            copyPrimArray newArr 0 sizes 0 len
+            let lastSize = indexPrimArray sizes (len - 1)
+            writePrimArray newArr (len - 1) (lastSize + 1)
             unsafeFreezePrimArray newArr
     snocTree _ (Leaf arr) = Leaf $ A.snoc arr x
 
@@ -692,7 +830,7 @@
         let newShift = (countTrailingZeros sz `div` blockShift) * blockShift
         in if newShift > sh then min else newShift
     computeShift _ sh min (Unbalanced arr sizes) =
-        let lastIdx = sizeofPrimArray sizes - 1
+        let lastIdx = length arr - 1
             sz' = indexPrimArray sizes lastIdx - indexPrimArray sizes (lastIdx - 1) -- the size of the last subtree
             newMin = if length arr < blockSize then sh else min
         in computeShift sz' (down sh) newMin (A.last arr)
@@ -707,44 +845,28 @@
 
 -- splitting
 
--- | \(O(\log n)\). The first @i@ elements of the vector.
--- If @i@ is negative, the empty vector is returned. If the vector contains less than @i@ elements, the whole vector is returned.
-take :: Int -> Vector a -> Vector a
-take _ Empty = Empty
-take n v@(Root size sh tree)
-    | n <= 0 = empty
-    | n >= size = v
-    | otherwise = normalize $ Root n sh (takeTree (n - 1) sh tree)
-  where
-    -- the initial @i@ is @n - 1@ -- the index of the last element in the new tree
-    takeTree i sh (Balanced arr) =
-        let idx = radixIndex i sh
-            newArr = A.take arr (idx + 1)
-        in Balanced (A.adjust' newArr idx (takeTree i (down sh)))
-    takeTree i sh (Unbalanced arr sizes) =
-        let (idx, subIdx) = relaxedRadixIndex sizes i sh
-            newArr = A.take arr (idx + 1)
-        in computeSizes sh (A.adjust' newArr idx (takeTree subIdx (down sh)))
-    takeTree i _ (Leaf arr) = Leaf (A.take arr ((i .&. blockMask) + 1))
+-- the initial @i@ is @n - 1@ -- the index of the last element in the new tree
+takeTree :: Int -> Shift -> Tree a -> Tree a
+takeTree i sh (Balanced arr) =
+    let idx = radixIndex i sh
+        newArr = A.take arr (idx + 1)
+    in Balanced (A.adjust' newArr idx (takeTree i (down sh)))
+takeTree i sh (Unbalanced arr sizes) =
+    let (idx, subIdx) = relaxedRadixIndex sizes i sh
+        newArr = A.take arr (idx + 1)
+    in computeSizes sh (A.adjust' newArr idx (takeTree subIdx (down sh)))
+takeTree i _ (Leaf arr) = Leaf (A.take arr ((i .&. blockMask) + 1))
 
--- | \(O(\log n)\). The vector without the first @i@ elements
--- If @i@ is negative, the whole vector is returned. If the vector contains less than @i@ elements, the empty vector is returned.
-drop :: Int -> Vector a -> Vector a
-drop _ Empty = Empty
-drop n v@(Root size sh tree)
-    | n <= 0 = v
-    | n >= size = empty
-    | otherwise = normalize $ Root (size - n) sh (dropTree n sh tree)
-  where
-    dropTree n sh (Balanced arr) =
-        let idx = radixIndex n sh
-            newArr = A.drop arr idx
-        in computeSizes sh (A.adjust' newArr 0 (dropTree n (down sh)))
-    dropTree n sh (Unbalanced arr sizes) =
-        let (idx, subIdx) = relaxedRadixIndex sizes n sh
-            newArr = A.drop arr idx
-        in computeSizes sh (A.adjust' newArr 0 (dropTree subIdx (down sh)))
-    dropTree n _ (Leaf arr) = Leaf (A.drop arr (n .&. blockMask))
+dropTree :: Int -> Shift -> Tree a -> Tree a
+dropTree n sh (Balanced arr) =
+    let idx = radixIndex n sh
+        newArr = A.drop arr idx
+    in computeSizes sh (A.adjust' newArr 0 (dropTree n (down sh)))
+dropTree n sh (Unbalanced arr sizes) =
+    let (idx, subIdx) = relaxedRadixIndex sizes n sh
+        newArr = A.drop arr idx
+    in computeSizes sh (A.adjust' newArr 0 (dropTree subIdx (down sh)))
+dropTree n _ (Leaf arr) = Leaf (A.drop arr (n .&. blockMask))
 
 normalize :: Vector a -> Vector a
 normalize (Root size sh (Balanced arr))
diff --git a/src/Data/RRBVector/Internal/Array.hs b/src/Data/RRBVector/Internal/Array.hs
--- a/src/Data/RRBVector/Internal/Array.hs
+++ b/src/Data/RRBVector/Internal/Array.hs
@@ -1,45 +1,48 @@
 {-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE MagicHash #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE UnboxedTuples #-}
 
--- Warning: No bound checks are performed!
+-- | This is an internal module.
+--
+-- It provides a thin wrapper over "Data.Primitive.SmallArray"
+-- with \(O(1)\) slicing.
+--
+-- __Warning:__ No bound checks are performed!
 
 module Data.RRBVector.Internal.Array
     ( Array, MutableArray
-    , empty, singleton, from2
+    , ifoldrStep, ifoldlStep, ifoldrStep', ifoldlStep'
+    , empty, singleton, from2, wrap
     , replicate, replicateSnoc
     , index, head, last
     , update, adjust, adjust'
     , take, drop, splitAt
-    , snoc, cons
+    , snoc, cons, (++)
     , map, map'
+    , imapStep, imapStep'
+    , unzipWith
     , traverse, traverse'
+    , itraverseStep, itraverseStep'
     , new, read, write
     , freeze, thaw
     ) where
 
-import Control.Applicative (Applicative(liftA2))
+#if !(MIN_VERSION_base(4,18,0))
+import Control.Applicative (liftA2)
+#endif
 import Control.DeepSeq (NFData(..))
 import Control.Monad (when)
 import Control.Monad.ST
 import Data.Foldable (Foldable(..))
 import Data.Primitive.SmallArray
-import Prelude hiding (replicate, take, drop, splitAt, head, last, map, traverse, read)
+import Prelude hiding (replicate, take, drop, splitAt, head, last, map, traverse, read, unzip, (++))
 
 -- start length array
 data Array a = Array !Int !Int !(SmallArray a)
 data MutableArray s a = MutableArray !Int !Int !(SmallMutableArray s a)
 
-instance Semigroup (Array a) where
-    Array start1 len1 arr1 <> Array start2 len2 arr2 = Array 0 len' $ runSmallArray $ do
-        sma <- newSmallArray len' uninitialized
-        copySmallArray sma 0 arr1 start1 len1
-        copySmallArray sma len1 arr2 start2 len2
-        pure sma
-      where
-        !len' = len1 + len2
-
 instance Foldable Array where
     foldr f z (Array start len arr) =
         let end = start + len
@@ -74,6 +77,36 @@
 instance (NFData a) => NFData (Array a) where
     rnf = foldl' (\_ x -> rnf x) ()
 
+ifoldrStep :: Int -> (a -> Int) -> (Int -> a -> b -> b) -> b -> Array a -> b
+ifoldrStep i0 step f z (Array start len arr) =
+    let end = start + len
+        go !i !j -- i is the index in arr, j is the index for f
+            | i == end = z
+            | (# x #) <- indexSmallArray## arr i = f j x (go (i + 1) (j + step x))
+    in go start i0
+
+ifoldlStep :: Int -> (a -> Int) -> (Int -> b -> a -> b) -> b -> Array a -> b
+ifoldlStep i0 step f z (Array start len arr) =
+    let go !i !j -- i is the index in arr, j is the index for f
+            | i < start = z
+            | (# x #) <- indexSmallArray## arr i = f j (go (i - 1) (j - step x)) x
+    in go (start + len - 1) i0
+
+ifoldrStep' :: Int -> (a -> Int) -> (Int -> a -> b -> b) -> b -> Array a -> b
+ifoldrStep' i0 step f z (Array start len arr) =
+    let go !i !j !acc -- i is the index in arr, j is the index for f
+            | i < start = acc
+            | (# x #) <- indexSmallArray## arr i = go (i - 1) (j - step x) (f j x acc)
+    in go (start + len - 1) i0 z
+
+ifoldlStep' :: Int -> (a -> Int) -> (Int -> b -> a -> b) -> b -> Array a -> b
+ifoldlStep' i0 step f z (Array start len arr) =
+    let end = start + len
+        go !i !j !acc -- i is the index in arr, j is the index for f
+            | i == end = acc
+            | (# x #) <- indexSmallArray## arr i = go (i + 1) (j + step x) (f j acc x)
+    in go start i0 z
+
 uninitialized :: a
 uninitialized = errorWithoutStackTrace "uninitialized"
 
@@ -89,6 +122,9 @@
     writeSmallArray sma 1 y
     pure sma
 
+wrap :: SmallArray a -> Array a
+wrap arr = Array 0 (sizeofSmallArray arr) arr
+
 replicate :: Int -> a -> Array a
 replicate n x = Array 0 n $ runSmallArray (newSmallArray n x)
 
@@ -140,21 +176,30 @@
 last arr = index arr (length arr - 1)
 
 snoc :: Array a -> a -> Array a
-snoc (Array _ len arr) x = Array 0 len' $ runSmallArray $ do
+snoc (Array start len arr) x = Array 0 len' $ runSmallArray $ do
     sma <- newSmallArray len' x
-    copySmallArray sma 0 arr 0 len
+    copySmallArray sma 0 arr start len
     pure sma
   where
     !len' = len + 1
 
 cons :: Array a -> a -> Array a
-cons (Array _ len arr) x = Array 0 len' $ runSmallArray $ do
+cons (Array start len arr) x = Array 0 len' $ runSmallArray $ do
     sma <- newSmallArray len' x
-    copySmallArray sma 1 arr 0 len
+    copySmallArray sma 1 arr start len
     pure sma
   where
     !len' = len + 1
 
+(++) :: Array a -> Array a -> Array a
+Array start1 len1 arr1 ++ Array start2 len2 arr2 = Array 0 len' $ runSmallArray $ do
+    sma <- newSmallArray len' uninitialized
+    copySmallArray sma 0 arr1 start1 len1
+    copySmallArray sma len1 arr2 start2 len2
+    pure sma
+  where
+    !len' = len1 + len2
+
 map :: (a -> b) -> Array a -> Array b
 map f (Array start len arr) = Array 0 len $ runSmallArray $ do
     sma <- newSmallArray len uninitialized
@@ -177,6 +222,46 @@
     loop start 0
     pure sma
 
+-- helper function for implementing imap
+imapStep :: Int -> (a -> Int) -> (Int -> a -> b) -> Array a -> Array b
+imapStep i0 step f (Array start len arr) = Array 0 len $ runSmallArray $ do
+    sma <- newSmallArray len uninitialized
+    -- i is the index in arr, j is the index in sma, k is the index for f
+    let loop !i !j !k = when (j < len) $ do
+            x <- indexSmallArrayM arr i
+            writeSmallArray sma j (f k x)
+            loop (i + 1) (j + 1) (k + step x)
+    loop start 0 i0
+    pure sma
+
+-- helper function for implementing imap
+imapStep' :: Int -> (a -> Int) -> (Int -> a -> b) -> Array a -> Array b
+imapStep' i0 step f (Array start len arr) = Array 0 len $ runSmallArray $ do
+    sma <- newSmallArray len uninitialized
+    -- i is the index in arr, j is the index in sma, k is the index for f
+    let loop !i !j !k = when (j < len) $ do
+            x <- indexSmallArrayM arr i
+            writeSmallArray sma j $! f k x
+            loop (i + 1) (j + 1) (k + step x)
+    loop start 0 i0
+    pure sma
+
+unzipWith :: (a -> (b, c)) -> Array a -> (Array b, Array c)
+unzipWith f (Array start len arr) = runST $ do
+    sma1 <- newSmallArray len uninitialized
+    sma2 <- newSmallArray len uninitialized
+    -- i is the index in arr, j is the index in sma1/sma2
+    let loop i j = when (j < len) $ do
+            val <- indexSmallArrayM arr i
+            let !(x, y) = f val
+            writeSmallArray sma1 j x
+            writeSmallArray sma2 j y
+            loop (i + 1) (j + 1)
+    loop start 0
+    arr1 <- unsafeFreezeSmallArray sma1
+    arr2 <- unsafeFreezeSmallArray sma2
+    pure (Array 0 len arr1, Array 0 len arr2)
+
 newtype STA a = STA (forall s. SmallMutableArray s a -> ST s (SmallArray a))
 
 runSTA :: Int -> STA a -> Array a
@@ -197,6 +282,24 @@
             | j == len = pure $ STA unsafeFreezeSmallArray
             | (# x #) <- indexSmallArray## arr i = liftA2 (\ !y (STA m) -> STA $ \sma -> writeSmallArray sma j y *> m sma) (f x) (go (i + 1) (j + 1))
     in runSTA len <$> go start 0
+
+-- helper function for implementing itraverse
+itraverseStep :: (Applicative f) => Int -> (a -> Int) -> (Int -> a -> f b) -> Array a -> f (Array b)
+itraverseStep i0 step f (Array start len arr) =
+    -- i is the index in arr, j is the index in sma, k is the index for f
+    let go !i !j !k
+            | j == len = pure $ STA unsafeFreezeSmallArray
+            | (# x #) <- indexSmallArray## arr i = liftA2 (\y (STA m) -> STA $ \sma -> writeSmallArray sma j y *> m sma) (f k x) (go (i + 1) (j + 1) (k + step x))
+    in runSTA len <$> go start 0 i0
+
+-- helper function for implementing itraverse
+itraverseStep' :: (Applicative f) => Int -> (a -> Int) -> (Int -> a -> f b) -> Array a -> f (Array b)
+itraverseStep' i0 step f (Array start len arr) =
+    -- i is the index in arr, j is the index in sma, k is the index for f
+    let go !i !j !k
+            | j == len = pure $ STA unsafeFreezeSmallArray
+            | (# x #) <- indexSmallArray## arr i = liftA2 (\ !y (STA m) -> STA $ \sma -> writeSmallArray sma j y *> m sma) (f k x) (go (i + 1) (j + 1) (k + step x))
+    in runSTA len <$> go start 0 i0
 
 new :: Int -> ST s (MutableArray s a)
 new len = MutableArray 0 len <$> newSmallArray len uninitialized
diff --git a/src/Data/RRBVector/Internal/Buffer.hs b/src/Data/RRBVector/Internal/Buffer.hs
--- a/src/Data/RRBVector/Internal/Buffer.hs
+++ b/src/Data/RRBVector/Internal/Buffer.hs
@@ -1,3 +1,8 @@
+-- | This is an internal module.
+--
+-- A 'Buffer' is an array with a fixed capacity, used to build up 'Data.RRBVector.Internal.Array.Array's.
+-- It is used in the implementation of 'Data.RRBVector.fromList' and 'Data.RRBVector.><'.
+
 module Data.RRBVector.Internal.Buffer
     ( Buffer
     , new
@@ -8,30 +13,41 @@
 
 import Control.Monad.ST
 
+import Data.Primitive.SmallArray
 import Data.RRBVector.Internal.IntRef
 import qualified Data.RRBVector.Internal.Array as A
 
-data Buffer s a = Buffer !(A.MutableArray s a) !(IntRef s)
+-- | A mutable array buffer with a fixed capacity.
+data Buffer s a = Buffer !(SmallMutableArray s a) !(IntRef s)
 
+-- | \(O(n)\). Create a new empty buffer with the given capacity.
 new :: Int -> ST s (Buffer s a)
 new capacity = do
-    buffer <- A.new capacity
+    buffer <- newSmallArray capacity uninitialized
     offset <- newIntRef 0
     pure (Buffer buffer offset)
 
+uninitialized :: a
+uninitialized = errorWithoutStackTrace "uninitialized"
+
+-- | \(O(1)\). Push a new element onto the buffer.
+-- The size of the buffer must not exceed the capacity, but this is not checked.
 push :: Buffer s a -> a -> ST s ()
 push (Buffer buffer offset) x = do
     idx <- readIntRef offset
-    A.write buffer idx x
+    writeSmallArray buffer idx x
     writeIntRef offset (idx + 1)
 
+-- | \(O(n)\). Freeze the content of the buffer and return it.
+-- This resets the buffer so that it is empty.
 get :: Buffer s a -> ST s (A.Array a)
 get (Buffer buffer offset) = do
     len <- readIntRef offset
-    result <- A.freeze buffer 0 len
+    result <- freezeSmallArray buffer 0 len
     writeIntRef offset 0
-    pure result
+    pure (A.wrap result)
 
+-- | \(O(1)\). Return the current size of the buffer.
 size :: Buffer s a -> ST s Int
 size (Buffer _ offset) = readIntRef offset
 {-# INLInE size #-}
diff --git a/src/Data/RRBVector/Internal/Debug.hs b/src/Data/RRBVector/Internal/Debug.hs
--- a/src/Data/RRBVector/Internal/Debug.hs
+++ b/src/Data/RRBVector/Internal/Debug.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE PatternSynonyms #-}
+
 {- |
 This module contains some debug utilities. It should only be used for debugging/testing purposes.
 -}
@@ -5,14 +7,22 @@
 module Data.RRBVector.Internal.Debug
     ( showTree
     , fromListUnbalanced
+    , pattern Empty, pattern Root
+    , Tree, Shift
+    , pattern Balanced, pattern Unbalanced, pattern Leaf
+    , Invariant, valid
     ) where
 
 import Control.Monad.ST (runST)
-import Data.Foldable (toList)
+import Data.Bits (shiftL)
+import Data.Foldable (foldl', toList, traverse_)
 import Data.List (intercalate)
-import Data.Primitive (primArrayToList)
+import Data.Primitive.PrimArray (PrimArray, primArrayToList, indexPrimArray, sizeofPrimArray)
 
-import Data.RRBVector.Internal
+import Data.RRBVector.Internal hiding (Empty, Root, Balanced, Unbalanced, Leaf)
+import qualified Data.RRBVector.Internal as RRB
+import Data.RRBVector.Internal.Array (Array)
+import qualified Data.RRBVector.Internal.Array as A
 import qualified Data.RRBVector.Internal.Buffer as Buffer
 
 -- | \(O(n)\). Show the underlying tree of a vector.
@@ -30,10 +40,10 @@
 --
 -- Note that it is not possbible to create an invalid 'Vector' with this function.
 fromListUnbalanced :: [a] -> Vector a
-fromListUnbalanced [] = Empty
+fromListUnbalanced [] = RRB.Empty
 fromListUnbalanced [x] = singleton x
-fromListUnbalanced ls = case nodes Leaf ls of
-    [tree] -> Root (treeSize 0 tree) 0 tree -- tree is a single leaf
+fromListUnbalanced ls = case nodes RRB.Leaf ls of
+    [tree] -> RRB.Root (treeSize 0 tree) 0 tree -- tree is a single leaf
     ls' -> iterateNodes blockShift ls'
   where
     n = blockSize - 1
@@ -57,5 +67,134 @@
     {-# INLINE nodes #-}
 
     iterateNodes sh trees = case nodes (computeSizes sh) trees of
-        [tree] -> Root (treeSize sh tree) sh tree
+        [tree] -> RRB.Root (treeSize sh tree) sh tree
         trees' -> iterateNodes (up sh) trees'
+
+pattern Empty :: Vector a
+pattern Empty <- RRB.Empty
+
+pattern Root :: Int -> Shift -> Tree a -> Vector a
+pattern Root size sh tree <- RRB.Root size sh tree
+
+{-# COMPLETE Empty, Root #-}
+
+pattern Balanced :: Array (Tree a) -> Tree a
+pattern Balanced arr <- RRB.Balanced arr
+
+pattern Unbalanced :: Array (Tree a) -> PrimArray Int -> Tree a
+pattern Unbalanced arr sizes <- RRB.Unbalanced arr sizes
+
+pattern Leaf :: Array a -> Tree a
+pattern Leaf arr <- RRB.Leaf arr
+
+{-# COMPLETE Balanced, Unbalanced, Leaf #-}
+
+-- | Structural invariants a vector is expected to hold.
+data Invariant
+    = RootSizeGt0      -- Root: Size > 0
+    | RootShiftDiv     -- Root: The shift at the root is divisible by blockShift
+    | RootSizeCorrect  -- Root: The size at the root is correct
+    | RootGt1Child     -- Root: The root has more than 1 child if not a Leaf
+    | BalShiftGt0      -- Balanced: Shift > 0
+    | BalNumChildren   -- Balanced: The number of children is blockSize unless
+                       -- the parent is unbalanced or the node is on the right
+                       -- edge in which case it is in [1,blockSize]
+    | BalFullChildren  -- Balanced: All children are full, except for the last
+                       -- if the node is on the right edge
+    | UnbalShiftGt0    -- Unbalanced: Shift > 0
+    | UnbalParentUnbal -- Unbalanced: Parent is Unbalanced
+    | UnbalNumChildren -- Unbalanced: The number of children is in [1,blockSize]
+    | UnbalSizes       -- Unbalanced: The sizes array is correct
+    | UnbalNotBal      -- Unbalanced: The tree is not full enough to be a
+                       -- Balanced
+    | LeafShift0       -- Leaf: Shift == 0
+    | LeafNumElems     -- Leaf: The number of elements is in [1,blockSize]
+    deriving Show
+
+assert :: Invariant -> Bool -> Either Invariant ()
+assert i False = Left i
+assert _ True = pure ()
+
+-- | Check tree invariants. Returns @Left@ on finding a violated invariant.
+valid :: Vector a -> Either Invariant ()
+valid RRB.Empty = pure ()
+valid (RRB.Root size sh tree) = do
+    assert RootSizeGt0 $ size > 0
+    assert RootShiftDiv $ sh `mod` blockShift == 0
+    assert RootSizeCorrect $ size == countElems tree
+    assert RootGt1Child $ case tree of
+        Balanced arr -> length arr > 1
+        Unbalanced arr _ -> length arr > 1
+        Leaf _ -> True
+    validTree Unbal sh tree
+
+data NodeDesc
+    = Bal           -- parent is Balanced
+    | BalRightEdge  -- parent is Balanced and this node is on the right edge
+    | Unbal         -- parent is Unbalanced
+
+validTree :: NodeDesc -> Shift -> Tree a -> Either Invariant ()
+validTree desc sh (RRB.Balanced arr) = do
+    assert BalShiftGt0 $ sh > 0
+    assert BalNumChildren $ case desc of
+        Bal -> n == blockSize
+        BalRightEdge -> n >= 1 && n <= blockSize
+        Unbal -> n >= 1 && n <= blockSize
+    assert BalFullChildren $
+        all (\t -> countElems t == 1 `shiftL` sh) expectedFullChildren
+    traverse_ (validTree Bal (down sh)) arrInit
+    validTree descLast (down sh) (A.last arr)
+  where
+    n = length arr
+    arrInit = A.take arr (n-1)
+    expectedFullChildren = case desc of
+        Bal -> arr
+        BalRightEdge -> arrInit
+        Unbal -> arrInit
+    descLast = case desc of
+        Bal -> Bal
+        BalRightEdge -> BalRightEdge
+        Unbal -> BalRightEdge
+validTree desc sh (RRB.Unbalanced arr sizes) = do
+    assert UnbalShiftGt0 $ sh > 0
+    case desc of
+        Bal -> assert UnbalParentUnbal False
+        BalRightEdge -> assert UnbalParentUnbal False
+        Unbal -> assert UnbalNumChildren $ n >= 1 && n <= blockSize
+    assert UnbalSizes $ n == sizeofPrimArray sizes
+    assert UnbalSizes $
+        all (\i -> countElems (A.index arr i) == getSize sizes i) [0 .. n-1]
+    assert UnbalNotBal $ not (couldBeBalanced sh arr sizes)
+    traverse_ (validTree Unbal (down sh)) arr
+  where
+    n = length arr
+validTree desc sh (RRB.Leaf arr) = do
+    assert LeafShift0 $ sh == 0
+    assert LeafNumElems $ case desc of
+        Bal -> n == blockSize
+        BalRightEdge -> n >= 1 && n <= blockSize
+        Unbal -> n >= 1 && n <= blockSize
+  where
+    n = length arr
+
+-- | Check whether an Unbalanced node could be Balanced.
+couldBeBalanced :: Shift -> A.Array (Tree a) -> PrimArray Int -> Bool
+couldBeBalanced sh arr sizes =
+   all (\i -> getSize sizes i == 1 `shiftL` sh) [0 .. n-2] &&
+   (case A.last arr of
+       Balanced _ -> True
+       Unbalanced arr' sizes' -> couldBeBalanced (down sh) arr' sizes'
+       Leaf _ -> True)
+  where
+    n = length arr
+
+getSize :: PrimArray Int -> Int -> Int
+getSize sizes 0 = indexPrimArray sizes 0
+getSize sizes i = indexPrimArray sizes i - indexPrimArray sizes (i-1)
+
+countElems :: Tree a -> Int
+countElems (RRB.Balanced arr) =
+    foldl' (\acc tree -> acc + countElems tree) 0 arr
+countElems (RRB.Unbalanced arr _) =
+    foldl' (\acc tree -> acc + countElems tree) 0 arr
+countElems (RRB.Leaf arr) = length arr
diff --git a/src/Data/RRBVector/Internal/Indexed.hs b/src/Data/RRBVector/Internal/Indexed.hs
deleted file mode 100644
--- a/src/Data/RRBVector/Internal/Indexed.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Data.RRBVector.Internal.Indexed where
-
--- TODO: use unboxed tuples?
-
-data WithIndex a = WithIndex !Int a
-
--- | > Compose (State Int) f a
-newtype Indexed f a = Indexed { runIndexed :: Int -> WithIndex (f a) }
-
-instance Functor f => Functor (Indexed f) where
-    fmap f (Indexed sf) = Indexed $ \s -> let WithIndex s' x = sf s in WithIndex s' (fmap f x)
-    {-# INLINE fmap #-}
-
-instance Applicative f => Applicative (Indexed f) where
-    pure x = Indexed $ \s -> WithIndex s (pure x)
-    {-# INLINE pure #-}
-
-    Indexed sfa <*> Indexed sfb = Indexed $ \s ->
-        let WithIndex s' f = sfa s
-            WithIndex s'' x = sfb s'
-        in WithIndex s'' (f <*> x)
-    {-# INLINE (<*>) #-}
-
-evalIndexed :: Indexed f a -> Int -> f a
-evalIndexed (Indexed sf) x = let WithIndex _ y = sf x in y
-{-# INLINE evalIndexed #-}
diff --git a/src/Data/RRBVector/Internal/IntRef.hs b/src/Data/RRBVector/Internal/IntRef.hs
--- a/src/Data/RRBVector/Internal/IntRef.hs
+++ b/src/Data/RRBVector/Internal/IntRef.hs
@@ -1,3 +1,7 @@
+-- | This is an internal module.
+--
+-- It works like "Data.STRef", but is specialized to 'Int' and more efficient.
+
 module Data.RRBVector.Internal.IntRef
     ( IntRef
     , newIntRef
@@ -8,8 +12,10 @@
 import Control.Monad.ST
 import Data.Primitive.PrimArray
 
+-- | A mutable 'Int' reference.
 newtype IntRef s = IntRef (MutablePrimArray s Int)
 
+-- | \(O(1)\). Create a new 'IntRef' that is initialized with @0@.
 newIntRef :: Int -> ST s (IntRef s)
 newIntRef i = do
     arr <- newPrimArray 1
@@ -17,10 +23,12 @@
     pure (IntRef arr)
 {-# INLINE newIntRef #-}
 
+-- | \(O(1)\). Read the content of the 'IntRef'.
 readIntRef :: IntRef s -> ST s Int
 readIntRef (IntRef arr) = readPrimArray arr 0
 {-# INLINE readIntRef #-}
 
+-- | \(O(1)\). Write a value to the 'IntRef'.
 writeIntRef :: IntRef s -> Int -> ST s ()
 writeIntRef (IntRef arr) = writePrimArray arr 0
 {-# INLINE writeIntRef #-}
diff --git a/src/Data/RRBVector/Internal/Sorting.hs b/src/Data/RRBVector/Internal/Sorting.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/RRBVector/Internal/Sorting.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MagicHash #-}
+
+module Data.RRBVector.Internal.Sorting
+    ( sort
+    , sortBy
+    , sortOn
+    ) where
+
+import Data.Foldable (toList)
+import Data.Foldable.WithIndex (ifor_)
+import Data.Primitive.Array
+import Data.SamSort (sortArrayBy)
+import Data.Semigroup (Arg(..))
+
+import Data.RRBVector.Internal
+
+uninitialized :: a
+uninitialized = errorWithoutStackTrace "uninitialized"
+
+-- | \(O(n \log n)\). Sort the vector in ascending order.
+-- The sort is stable, meaning the order of equal elements is preserved.
+--
+-- @since 0.2.2.0
+sort :: (Ord a) => Vector a -> Vector a
+sort = sortBy compare
+
+-- | \(O(n \log n)\). Sort the vector in ascending order according to the specified comparison function.
+-- The sort is stable, meaning the order of equal elements is preserved.
+--
+-- @since 0.2.2.0
+sortBy :: (a -> a -> Ordering) -> Vector a -> Vector a
+sortBy cmp v =
+    let sortedArr = createArray (length v) uninitialized $ \arr@(MutableArray arr#) -> do
+            ifor_ v (writeArray arr)
+            sortArrayBy cmp arr# 0 (length v)
+    in fromList . toList $ sortedArr
+
+-- | \(O(n \log n)\). Sort the vector in ascending order by comparing the results of applying the key function to each element.
+-- The sort is stable, meaning the order of equal elements is preserved.
+-- @`sortOn` f@ is equivalent to @`sortBy` (`Data.Ord.comparing` f)@, but only evaluates @f@ once for each element.
+--
+-- @since 0.2.2.0
+sortOn :: (Ord b) => (a -> b) -> Vector a -> Vector a
+sortOn f v =
+    let sortedArr = createArray (length v) uninitialized $ \arr@(MutableArray arr#) -> do
+            ifor_ v $ \i x -> let !y = f x in writeArray arr i (Arg y x)
+            sortArrayBy compare arr# 0 (length v)
+    in fromList . fmap (\(Arg _ x) -> x) . toList $ sortedArr
diff --git a/test/Arbitrary.hs b/test/Arbitrary.hs
--- a/test/Arbitrary.hs
+++ b/test/Arbitrary.hs
@@ -1,11 +1,40 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE FlexibleInstances #-}
+
 module Arbitrary where
 
+#if !(MIN_VERSION_base(4,18,0))
+import Control.Applicative (liftA2)
+#endif
+import Data.Foldable (toList)
+
 import Test.Tasty.QuickCheck
 
 import qualified Data.RRBVector as V
 import Data.RRBVector.Internal.Debug
 
--- TODO: improve instance
+builders :: [[a] -> V.Vector a]
+builders = [V.fromList, fromListUnbalanced]
+
 instance (Arbitrary a) => Arbitrary (V.Vector a) where
-    arbitrary = oneof [V.fromList <$> arbitrary, fromListUnbalanced <$> arbitrary]
+    arbitrary = arbitrary1
+    shrink = shrink1
 
+-- TODO: improve instance
+instance Arbitrary1 V.Vector where
+    liftArbitrary gen = do
+        build <- elements builders
+        fmap build (liftArbitrary gen)
+
+    liftShrink shr = concatMap (sequence builders) . liftShrink shr . toList
+
+-- A custom 'Testable' instance to use 'showTree'.
+instance {-# OVERLAPPING #-} (Arbitrary a, Show a, Testable prop) => Testable (V.Vector a -> prop) where
+    property = propertyForAllShrinkShow arbitrary shrink (pure . showTree)
+
+    propertyForAllShrinkShow gen shr shw f =
+        propertyForAllShrinkShow
+            (liftA2 (,) gen arbitrary)
+            (liftShrink2 shr shrink)
+            (\(x, y) -> shw x ++ [showTree y])
+            (uncurry f)
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE BangPatterns #-}
-
 import Test.Tasty
 import Test.Tasty.QuickCheck
 
diff --git a/test/Properties.hs b/test/Properties.hs
--- a/test/Properties.hs
+++ b/test/Properties.hs
@@ -1,15 +1,24 @@
+{-# LANGUAGE CPP #-}
+
+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
+
 module Properties
     ( properties
     ) where
 
-import Data.Foldable (toList)
-import Data.List (uncons)
+#if !(MIN_VERSION_base(4,18,0))
+import Control.Applicative (liftA2)
+#endif
+import Data.Foldable (Foldable(..))
+import Data.List (sort, sortBy, sortOn, uncons)
+import Data.Ord (comparing)
+import Data.Proxy (Proxy(..))
 import Prelude hiding ((==)) -- use @===@ instead
 
-import Data.Foldable.WithIndex
-import Data.Functor.WithIndex
-import Data.Traversable.WithIndex
+import qualified Data.Sequence as Seq
 import qualified Data.RRBVector as V
+import qualified Data.RRBVector.Internal.Debug as VDebug
+import Test.QuickCheck.Classes.Base
 import Test.Tasty
 import Test.Tasty.QuickCheck
 
@@ -19,6 +28,8 @@
 
 type V = V.Vector
 
+-- List equivalents of @Data.RRBVector@ functions
+
 lookupList :: Int -> [a] -> Maybe a
 lookupList i ls
     | i < length ls = Just (ls !! i)
@@ -46,39 +57,23 @@
 unsnoc [] = Nothing
 unsnoc ls = Just (init ls, last ls)
 
-instances :: TestTree
-instances = testGroup "instances"
-    [ testGroup "Foldable"
-        [ testProperty "foldr" $ \(v :: V Int) -> foldr (:) [] v === foldr (:) [] (toList v)
-        , testProperty "foldl" $ \(v :: V Int) -> foldl (flip (:)) [] v === foldl (flip (:)) [] (toList v)
-        ]
-    , testGroup "FoldableWithIndex"
-        [ testProperty "ifoldr" $ \(v :: V Int) -> ifoldr (\i x acc -> (i, x) : acc) [] v === ifoldr (\i x acc -> (i, x) : acc) [] (toList v)
-        , testProperty "ifoldl" $ \(v :: V Int) -> ifoldl (\i acc x -> (i, x) : acc) [] v === ifoldl (\i acc x -> (i, x) : acc)  [] (toList v)
-        , testProperty "satisfies `ifoldr (const f) x v = foldr f x v`" $
-            \(v :: V Int) -> ifoldr (const (:)) [] v === foldr (:) [] v
-        , testProperty "satisfies `ifoldl (const f) x v = foldl f x v`" $
-            \(v :: V Int) -> ifoldl (const (flip (:))) [] v === foldl (flip (:)) [] v
-        ]
-    , testGroup "Functor"
-        [ testProperty "fmap" $ \v -> toList (V.map (+ 1) v) === map (+ 1) (toList v)
-        ]
-    , testGroup "FunctorWithIndex"
-        [ testProperty "imap" $ \(v :: V Int) -> toList (imap (,) v) === imap (,) (toList v)
-        , testProperty "satisfies `imap (const f) v = map f v`" $ \v -> imap (const (+ 1)) v === V.map (+ 1) v
-        ]
-    , testGroup "Traversable"
-        [ testProperty "traverse" $
-            \(v :: V Int) -> fmap toList (traverse (Just . (+ 1)) v) === traverse (Just . (+ 1)) (toList v)
-        ]
-    , testGroup "TraversableWithIndex"
-        [ testProperty "itraverse" $
-            \(v :: V Int) -> fmap toList (itraverse (\i x -> Just (i + x)) v) === itraverse (\i x -> Just (i + x)) (toList v)
-        , testProperty "satisfies `itraverse (const f) v = traverse f v`" $
-            \(v :: V Int) -> itraverse (const (Just . (+ 1))) v === traverse (Just . (+ 1)) v
-        ]
-    ]
 
+testLaws :: Laws -> TestTree
+testLaws (Laws name pairs) = testGroup name (map (uncurry testProperty) pairs)
+
+proxyVInt :: Proxy (V Int)
+proxyVInt = Proxy
+
+proxyV :: Proxy V
+proxyV = Proxy
+
+checkValid :: Show a => V a -> Property
+checkValid v = case VDebug.valid v of
+    Left invariant ->
+        counterexample ("Invariant violated: " ++ show invariant) $
+        counterexample (VDebug.showTree v) False
+    _ -> property ()
+
 properties :: TestTree
 properties = testGroup "properties"
     [ testGroup "fromList"
@@ -86,22 +81,27 @@
         , testProperty "satisfies `toList . fromList = id`" $ \ls -> toList (V.fromList ls) === ls
         , testProperty "satisfies `fromList [] = empty`" $ V.fromList [] === V.empty
         , testProperty "satisfies `fromList [x] = singleton x`" $ \x -> V.fromList [x] === V.singleton x
+        , testProperty "valid" $ \xs -> checkValid (V.fromList xs)
         ]
     , testGroup "replicate"
         [ testProperty "satisifes `replicate n == fromList . replicate n`" $ \(Positive n) x -> V.replicate n x === V.fromList (replicate n x)
         , testProperty "returns the empty vector for non-positive n" $ \(NonPositive n) x -> V.replicate n x === V.empty
+        , testProperty "valid" $ \n x -> checkValid (V.replicate n x)
         ]
     , testGroup "<|"
         [ testProperty "prepends an element" $ \x v -> toList (x V.<| v) === x : toList v
         , testProperty "works for the empty vector" $ \x -> x V.<| V.empty === V.singleton x
+        , testProperty "valid" $ \x v -> checkValid (x V.<| v)
         ]
     , testGroup "|>"
         [ testProperty "appends an element" $ \v x -> toList (v V.|> x) === toList v ++ [x]
         , testProperty "works for the empty vector" $ \x -> V.empty V.|> x === V.singleton x
+        , testProperty "valid" $ \v x -> checkValid (v V.|> x)
         ]
     , testGroup "><"
         [ testProperty "concatenates two vectors" $ \v1 v2 -> toList (v1 V.>< v2) === toList v1 ++ toList v2
         , testProperty "works for the empty vector" $ \v -> (V.empty V.>< v === v) .&&. (v V.>< V.empty === v)
+        , testProperty "valid" $ \v1 v2 -> checkValid (v1 V.>< v2)
         ]
     , testGroup "lookup"
         [ testProperty "gets the element at the index" $ \v (NonNegative i) -> V.lookup i v === lookupList i (toList v)
@@ -110,57 +110,174 @@
     , testGroup "update"
         [ testProperty "updates the element at the index" $ \v (NonNegative i) x -> toList (V.update i x v) === updateList i x (toList v)
         , testProperty "returns the vector for negative indices" $ \v (Negative i) x -> V.update i x v === v
+        , testProperty "valid" $ \v i x -> checkValid (V.update i x v)
         ]
     , testGroup "adjust"
         [ testProperty "adjusts the element at the index" $ \v (NonNegative i) (Fn f) -> toList (V.adjust i f v) === adjustList i f (toList v)
         , testProperty "returns the vector for negative indices" $ \v (Negative i) (Fn f) -> V.adjust i f v === v
+        , testProperty "valid" $ \v i (Fn f) -> checkValid (V.adjust i f v)
         ]
     , testGroup "adjust'"
         [ testProperty "adjusts the element at the index" $ \v (NonNegative i) (Fn f) -> toList (V.adjust' i f v) === adjustList i f (toList v)
         , testProperty "returns the vector for negative indices" $ \v (Negative i) (Fn f) -> V.adjust' i f v === v
+        , testProperty "valid" $ \v i (Fn f) -> checkValid (V.adjust' i f v)
         ]
     , testGroup "viewl"
         [ testProperty "works like uncons" $ \v -> fmap (\(x, xs) -> (x, toList xs)) (V.viewl v) === uncons (toList v)
         , testProperty "works for the empty vector" $ V.viewl V.empty === Nothing
+        , testProperty "valid" $ \v -> fmap (checkValid . snd) (V.viewl v)
         ]
     , testGroup "viewr"
         [ testProperty "works like unsnoc" $ \v -> fmap (\(xs, x) -> (toList xs, x)) (V.viewr v) === unsnoc (toList v)
         , testProperty "works for the empty vector" $ V.viewr V.empty === Nothing
+        , testProperty "valid" $ \v -> fmap (checkValid . fst) (V.viewr v)
         ]
     , testGroup "take"
         [ testProperty "takes n elements" $ \v (Positive n) -> toList (V.take n v) === take n (toList v)
         , testProperty "returns the empty vector for non-positive n" $ \v (NonPositive n) -> V.take n v === V.empty
+        , testProperty "valid" $ \v n -> checkValid (V.take n v)
         ]
     , testGroup "drop"
         [ testProperty "drops n elements" $ \v (Positive n) -> toList (V.drop n v) === drop n (toList v)
         , testProperty "returns the vector for non-positive n" $ \v (NonPositive n) -> V.drop n v === v
+        , testProperty "valid" $ \v n -> checkValid (V.drop v n)
         ]
     , testGroup "splitAt"
         [ testProperty "splits the vector" $ \v n -> let (v1, v2) = V.splitAt n v in (toList v1, toList v2) === splitAt n (toList v)
+        , testProperty "valid" $ \v n -> let (v1, v2) = V.splitAt n v in checkValid v1 .&&. checkValid v2
         ]
     , testGroup "insertAt"
         [ testProperty "inserts an element" $ \v i x -> toList (V.insertAt i x v) === insertAtList i x (toList v)
+        , testProperty "prepends for negative indices" $ \v (Negative i) x -> V.insertAt i x v === x V.<| v
+        , testProperty "appends for too large indices" $ \v x -> forAll (arbitrary `suchThat` (> length v)) $ \i -> V.insertAt i x v === v V.|> x
         , testProperty "satisfies `insertAt 0 x v = x <| v`" $ \v x -> V.insertAt 0 x v === x V.<| v
         , testProperty "satisfies `insertAt (length v) x v = v |> x`" $ \v x -> V.insertAt (length v) x v === v V.|> x
+        , testProperty "valid" $ \v i x -> checkValid (V.insertAt i x v)
         ]
     , testGroup "deleteAt"
         [ testProperty "deletes an element" $ \v (NonNegative i) -> toList (V.deleteAt i v) === deleteAtList i (toList v)
         , testProperty "returns the vector for negative indices" $ \v (Negative i) -> V.deleteAt i v === v
+        , testProperty "returns the vector for too large indices" $ \v -> forAll (arbitrary `suchThat` (>= length v)) $ \i -> V.deleteAt i v === v
         , testProperty "satisfies `deleteAt 0 v = drop 1 v`" $ \v -> V.deleteAt 0 v === V.drop 1 v
         , testProperty "satisfies `deleteAt (length v - 1) v = take (length v - 1) v`" $ \v -> V.deleteAt (length v - 1) v === V.take (length v - 1) v
+        , testProperty "valid" $ \v i -> checkValid (V.deleteAt i v)
         ]
+    , testGroup "findIndexL"
+        [ testProperty "finds the first index" $ \v (Fn f) -> V.findIndexL f v === Seq.findIndexL f (Seq.fromList (toList v))
+        , testProperty "returns Nothing for the empty vector" $ \(Fn f) -> V.findIndexL f V.empty === Nothing
+        ]
+    , testGroup "findIndexR"
+        [ testProperty "finds the last index" $ \v (Fn f) -> V.findIndexR f v === Seq.findIndexR f (Seq.fromList (toList v))
+        , testProperty "returns Nothing for the empty vector" $ \(Fn f) -> V.findIndexR f V.empty === Nothing
+        ]
+    , localOption (QuickCheckMaxSize 1000) $ testGroup "findIndicesL"
+        [ testProperty "finds the indices starting from the left" $ \v (Fn f) -> V.findIndicesL f v === Seq.findIndicesL f (Seq.fromList (toList v))
+        , testProperty "returns [] for the empty vector" $ \(Fn f) -> V.findIndicesL f V.empty === []
+        ]
+    , localOption (QuickCheckMaxSize 1000) $ testGroup "findIndicesR"
+        [ testProperty "finds the indices starting from the right" $ \v (Fn f) -> V.findIndicesR f v === Seq.findIndicesR f (Seq.fromList (toList v))
+        , testProperty "returns [] for the empty vector" $ \(Fn f) -> V.findIndicesR f V.empty === []
+        ]
     , testGroup "reverse"
         [ testProperty "reverses the vector" $ \v -> toList (V.reverse v) === reverse (toList v)
+        , testProperty "valid" $ \v -> checkValid (V.reverse v)
         ]
     , testGroup "zip"
         [ testProperty "zips two vectors" $ \v1 v2 -> toList (V.zip v1 v2) === zip (toList v1) (toList v2)
+        , testProperty "valid" $ \v1 v2 -> checkValid (V.zip v1 v2)
         ]
     , testGroup "zipWith"
         [ testProperty "zips two vectors with a function" $ \v1 v2 -> toList (V.zipWith (+) v1 v2) === zipWith (+) (toList v1) (toList v2)
         , testProperty "satisfies `zipWith (,) v1 v2 = zip v1 v2`" $ \v1 v2 -> V.zipWith (,) v1 v2 === V.zip v1 v2
+        , testProperty "valid" $ \v1 v2 (Fn2 f) -> checkValid (V.zipWith f v1 v2)
         ]
     , testGroup "unzip"
         [ testProperty "unzips the vector" $ \v -> (\(xs, ys) -> (toList xs, toList ys)) (V.unzip v) === unzip (toList v)
+        , testProperty "valid" $ \v -> let (v1, v2) = V.unzip v in checkValid v1 .&&. checkValid v2
         ]
+    , localOption (QuickCheckMaxSize 1000) $ testGroup "sorting"
+        [ testProperty "sort" $ \v -> toList (V.sort v) === sort (toList v)
+        , testProperty "sortBy" $ \v -> let cmp = comparing fst in toList (V.sortBy cmp v) === sortBy cmp (toList v)
+        , testProperty "sortOn" $ \v -> let f = odd in toList (V.sortOn f v) === sortOn f (toList v)
+        ]
     , instances
+    , laws
+    , issues
+    ]
+
+instances :: TestTree
+instances = testGroup "instances"
+    [ testGroup "Foldable"
+        [ testProperty "foldr" $ \(v :: V Int) -> foldr (:) [] v === foldr (:) [] (toList v)
+        , testProperty "foldl" $ \(v :: V Int) -> foldl (flip (:)) [] v === foldl (flip (:)) [] (toList v)
+        , testProperty "foldr'" $ \(v :: V Int) -> foldr' (:) [] v === foldr' (:) [] (toList v)
+        , testProperty "foldl'" $ \(v :: V Int) -> foldl' (flip (:)) [] v === foldl' (flip (:)) [] (toList v)
+        ]
+    , testGroup "FoldableWithIndex"
+        [ testProperty "ifoldr" $ \(v :: V Int) -> V.ifoldr (\i x acc -> (i, x) : acc) [] v === V.ifoldr (\i x acc -> (i, x) : acc) [] (toList v)
+        , testProperty "ifoldl" $ \(v :: V Int) -> V.ifoldl (\i acc x -> (i, x) : acc) [] v === V.ifoldl (\i acc x -> (i, x) : acc)  [] (toList v)
+        , testProperty "ifoldr'" $ \(v :: V Int) -> V.ifoldr' (\i x acc -> (i, x) : acc) [] v === V.ifoldr' (\i x acc -> (i, x) : acc) [] (toList v)
+        , testProperty "ifoldl'" $ \(v :: V Int) -> V.ifoldl' (\i acc x -> (i, x) : acc) [] v === V.ifoldl' (\i acc x -> (i, x) : acc)  [] (toList v)
+        , testProperty "satisfies `ifoldr (const f) x v = foldr f x v`" $
+            \(v :: V Int) -> V.ifoldr (const (:)) [] v === foldr (:) [] v
+        , testProperty "satisfies `ifoldl (const f) x v = foldl f x v`" $
+            \(v :: V Int) -> V.ifoldl (const (flip (:))) [] v === foldl (flip (:)) [] v
+        ]
+    , testGroup "Functor"
+        [ testProperty "fmap" $ \v -> toList (V.map (+ 1) v) === map (+ 1) (toList v)
+        ]
+    , testGroup "FunctorWithIndex"
+        [ testProperty "imap" $ \(v :: V Int) -> toList (V.imap (,) v) === V.imap (,) (toList v)
+        , testProperty "satisfies `imap (const f) v = map f v`" $ \v -> V.imap (const (+ 1)) v === V.map (+ 1) v
+        ]
+    , testGroup "Traversable"
+        [ testProperty "traverse" $
+            \(v :: V Int) -> fmap toList (traverse (Just . (+ 1)) v) === traverse (Just . (+ 1)) (toList v)
+        ]
+    , testGroup "TraversableWithIndex"
+        [ testProperty "itraverse" $
+            \(v :: V Int) -> fmap toList (V.itraverse (\i x -> Just (i + x)) v) === V.itraverse (\i x -> Just (i + x)) (toList v)
+        , testProperty "satisfies `itraverse (const f) v = traverse f v`" $
+            \(v :: V Int) -> V.itraverse (const (Just . (+ 1))) v === traverse (Just . (+ 1)) v
+        , testProperty "satisfies `imapDefault f v = imap f v`" $
+            \(v :: V Int) -> V.imapDefault (,) v === V.imap (,) v
+        ]
+    , localOption (QuickCheckMaxSize 100) $ testGroup "Applicative"
+        [ testProperty "liftA2" $
+            \(v1 :: V Int) (v2 :: V Int) -> toList (liftA2 (,) v1 v2) === liftA2 (,) (toList v1) (toList v2)
+        , testProperty "<*>" $
+            \(v1 :: V Int) (v2 :: V Int) -> toList ((,) <$> v1 <*> v2) === ((,) <$> toList v1 <*> toList v2)
+        , testProperty "*>" $
+            \(v1 :: V Int) (v2 :: V Int) -> toList (v1 *> v2) === (toList v1 *> toList v2)
+        , testProperty "<*" $
+            \(v1 :: V Int) (v2 :: V Int) -> toList (v1 <* v2) === (toList v1 <* toList v2)
+        ]
+    ]
+
+laws :: TestTree
+laws = testGroup "typeclass laws"
+    [ testLaws $ eqLaws proxyVInt
+    , testLaws $ ordLaws proxyVInt
+    , testLaws $ isListLaws proxyVInt
+    , testLaws $ monoidLaws proxyVInt
+    , localOption (QuickCheckTests 500) . localOption (QuickCheckMaxSize 1000) . testLaws $ semigroupLaws proxyVInt
+    , localOption (QuickCheckMaxSize 500) . testLaws $ showLaws proxyVInt
+    , localOption (QuickCheckMaxSize 1000) . testLaws $ showReadLaws proxyVInt
+    , testLaws $ alternativeLaws proxyV
+    , localOption (QuickCheckTests 100) . localOption (QuickCheckMaxSize 100) . testLaws $ applicativeLaws proxyV
+    , localOption (QuickCheckTests 100) . testLaws $ foldableLaws proxyV
+    , testLaws $ functorLaws proxyV
+    , localOption (QuickCheckTests 100) . localOption (QuickCheckMaxSize 100) . testLaws $ monadLaws proxyV
+    , testLaws $ monadPlusLaws proxyV
+    , localOption (QuickCheckTests 500) . localOption (QuickCheckMaxSize 5000) . testLaws $ monadZipLaws proxyV
+    , localOption (QuickCheckMaxSize 100) . testLaws $ traversableLaws proxyV
+    ]
+
+-- old issues, to avoid regressions
+issues :: TestTree
+issues = testGroup "issues"
+    -- https://github.com/konsumlamm/rrb-vector/issues/10
+    [ testProperty "#10" $ \x v -> case V.viewl v of
+            Nothing -> property True
+            Just (_, v') -> x V.<| v' === V.update 0 x v
     ]
diff --git a/test/Strictness.hs b/test/Strictness.hs
--- a/test/Strictness.hs
+++ b/test/Strictness.hs
@@ -1,16 +1,17 @@
-{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE CPP #-}
 
-#define GHC_HEAP_VIEW defined(VERSION_ghc_heap_view)
-
 module Strictness
     ( strictness
     ) where
 
-#if GHC_HEAP_VIEW
+#ifdef VERSION_nothunks
 import Control.DeepSeq (deepseq)
-import GHC.AssertNF (isNF)
+import Data.Foldable (foldr', foldl', toList)
+import Data.Maybe (isNothing)
+import Data.RRBVector.Internal.Debug
+import NoThunks.Class
 #endif
+
 import qualified Data.RRBVector as V
 import Test.Tasty
 import Test.Tasty.QuickCheck
@@ -19,10 +20,23 @@
 
 default (Int)
 
-#if GHC_HEAP_VIEW
-testNF :: a -> Property
-testNF !x = ioProperty (isNF x)
+#ifdef VERSION_nothunks
+instance (NoThunks a) => NoThunks (V.Vector a) where
+    showTypeOf _ = "Vector"
 
+    wNoThunks _ Empty = pure Nothing
+    wNoThunks ctx (Root _ _ tree) = wNoThunks ctx tree
+
+instance (NoThunks a) => NoThunks (Tree a) where
+    showTypeOf _ = "Tree"
+
+    wNoThunks ctx (Balanced arr) = noThunksInValues ctx (toList arr)
+    wNoThunks ctx (Unbalanced arr _) = noThunksInValues ctx (toList arr)
+    wNoThunks ctx (Leaf arr) = noThunksInValues ctx (toList arr)
+
+testNF :: (NoThunks a) => a -> Property
+testNF x = x `seq` ioProperty (isNothing <$> wNoThunks [] x)
+
 tailVector :: V.Vector a -> Maybe (V.Vector a)
 tailVector v = case V.viewl v of
     Nothing -> Nothing
@@ -36,9 +50,9 @@
 
 strictness :: TestTree
 strictness = testGroup "strictness"
-#if GHC_HEAP_VIEW
-    [ testGroup "nf"
-        [ testProperty "empty" $ testNF V.empty
+#ifdef VERSION_nothunks
+    [ localOption (QuickCheckTests 500) $ testGroup "nf"
+        [ testProperty "empty" $ testNF (V.empty :: V.Vector Int)
         , testProperty "singleton" $ testNF (V.singleton 42)
         , testProperty "fromList" $ \ls -> ls `deepseq` testNF (V.fromList ls)
         , testProperty "replicate" $ \n -> testNF (V.replicate n 42)
@@ -54,7 +68,14 @@
         , testProperty "deleteAt" $ \v i -> v `deepseq` testNF (V.deleteAt i v)
         , testProperty "viewl (tail)" $ \v -> v `deepseq` testNF (tailVector v)
         , testProperty "viewr (init)" $ \v -> v `deepseq` testNF (initVector v)
+        , testProperty "map'" $ \v -> v `deepseq` testNF (V.map' (+ 1) v)
         , testProperty "reverse" $ \v -> v `deepseq` testNF (V.reverse v)
+        , testProperty "zip" $ \v1 v2 -> v1 `deepseq` v2 `deepseq` testNF (V.zip v1 v2)
+        , testProperty "unzip" $ \v -> v `deepseq` testNF (V.unzip v)
+        , testProperty "foldr'" $ \v -> (v :: V.Vector Int) `deepseq` testNF (foldr' (:) [] v)
+        , testProperty "foldl'" $ \v -> (v :: V.Vector Int) `deepseq` testNF (foldl' (flip (:)) [] v)
+        , testProperty "ifoldr'" $ \v -> (v :: V.Vector Int) `deepseq` testNF (V.ifoldr' (const (:)) [] v)
+        , testProperty "ifoldl'" $ \v -> (v :: V.Vector Int) `deepseq` testNF (V.ifoldl' (const (flip (:))) [] v)
         ]
     , testGroup "bottom"
 #else
@@ -69,5 +90,6 @@
         , testProperty "adjust" $ \v i -> V.adjust i (const undefined) v `seq` ()
         , testProperty "insertAt" $ \v i -> V.insertAt i undefined v `seq` ()
         , testProperty "map" $ \v -> V.map (const undefined) v `seq` ()
+        , testProperty "zipWith" $ \v1 v2 -> V.zipWith (\_ _ -> undefined) v1 v2 `seq` ()
         ]
     ]
