diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,20 +1,30 @@
-# poly [![Build Status](https://travis-ci.org/Bodigrim/poly.svg)](https://travis-ci.org/Bodigrim/poly) [![Hackage](http://img.shields.io/hackage/v/poly.svg)](https://hackage.haskell.org/package/poly) [![Hackage CI](https://matrix.hackage.haskell.org/api/v2/packages/poly/badge)](https://matrix.hackage.haskell.org/package/poly) [![Stackage LTS](http://stackage.org/package/poly/badge/lts)](http://stackage.org/lts/package/poly) [![Stackage Nightly](http://stackage.org/package/poly/badge/nightly)](http://stackage.org/nightly/package/poly)
-
-
+# poly [![Hackage](https://img.shields.io/hackage/v/poly.svg)](https://hackage.haskell.org/package/poly) [![Stackage LTS](https://www.stackage.org/package/poly/badge/lts)](https://www.stackage.org/lts/package/poly) [![Stackage Nightly](https://www.stackage.org/package/poly/badge/nightly)](https://www.stackage.org/nightly/package/poly) [![Coverage Status](https://coveralls.io/repos/github/Bodigrim/poly/badge.svg)](https://coveralls.io/github/Bodigrim/poly)
 
-Haskell library for univariate polynomials, backed by `Vector`.
+Haskell library for univariate and multivariate polynomials, backed by `Vector`s.
 
 ```haskell
+> -- Univariate polynomials
 > (X + 1) + (X - 1) :: VPoly Integer
-2 * X + 0
-
+2 * X
 > (X + 1) * (X - 1) :: UPoly Int
-1 * X^2 + 0 * X + (-1)
+1 * X^2 + (-1)
+
+> -- Multivariate polynomials
+> (X + Y) * (X - Y) :: VMultiPoly 2 Integer
+1 * X^2 + (-1) * Y^2
+> (X + Y + Z) ^ 2 :: UMultiPoly 3 Int
+1 * X^2 + 2 * X * Y + 2 * X * Z + 1 * Y^2 + 2 * Y * Z + 1 * Z^2
+
+> -- Laurent polynomials
+> (X^-2 + 1) * (X - X^-1) :: VLaurent Integer
+1 * X + (-1) * X^-3
+> (X^-1 + Y) * (X + Y^-1) :: UMultiLaurent 2 Int
+1 * X * Y + 2 + 1 * X^-1 * Y^-1
 ```
 
 ## Vectors
 
-`Poly v a` is polymorphic over a container `v`, implementing `Vector` interface, and coefficients of type `a`. Usually `v` is either a boxed vector from `Data.Vector` or an unboxed vector from `Data.Vector.Unboxed`. Use unboxed vectors whenever possible, e. g., when coefficients are `Int` or `Double`.
+`Poly v a` is polymorphic over a container `v`, implementing the `Vector` interface, and coefficients of type `a`. Usually `v` is either a boxed vector from [`Data.Vector`](https://hackage.haskell.org/package/vector/docs/Data-Vector.html) or an unboxed vector from [`Data.Vector.Unboxed`](https://hackage.haskell.org/package/vector/docs/Data-Vector-Unboxed.html). Use unboxed vectors whenever possible, e. g., when the coefficients are `Int`s or `Double`s.
 
 There are handy type synonyms:
 
@@ -32,9 +42,9 @@
 1 * X^2 + (-3) * X + 2
 ```
 
-(Unfortunately, a type is often ambiguous and must be given explicitly.)
+(Unfortunately, types are often ambiguous and must be given explicitly.)
 
-While being convenient to read and write in REPL, `X` is relatively slow. The fastest approach is to use `toPoly`, providing it with a vector of coefficients (head is the constant term):
+While being convenient to read and write in REPL, `X` is relatively slow. The fastest approach is to use `toPoly`, providing it with a vector of coefficients (constant term first):
 
 ```haskell
 > toPoly (Data.Vector.Unboxed.fromList [2, -3, 1 :: Int])
@@ -64,7 +74,7 @@
 1 * X^4 + 0 * X^3 + 0 * X^2 + 0 * X + (-1)
 ```
 
-One can also find convenient to `scale` by monomial (cf. `monomial` above):
+One can also find it convenient to `scale` by a monomial (cf. `monomial` above):
 
 ```haskell
 > scale 2 3.5 (X^2 + 1) :: UPoly Double
@@ -72,8 +82,8 @@
 ```
 
 While `Poly` cannot be made an instance of `Integral` (because there is no meaningful `toInteger`),
-it is an instance of `GcdDomain` and `Euclidean` from `semirings` package. These type classes
-cover main functionality of `Integral`, providing division with remainder and `gcd` / `lcm`:
+it is an instance of `GcdDomain` and `Euclidean` from the [`semirings`](https://hackage.haskell.org/package/semirings) package. These type classes
+cover the main functionality of `Integral`, providing division with remainder and `gcd` / `lcm`:
 
 ```haskell
 > Data.Euclidean.gcd (X^2 + 7 * X + 6) (X^2 - 5 * X - 6) :: UPoly Int
@@ -83,8 +93,8 @@
 (1.0 * X + 0.0,1.0 * X + 2.0)
 ```
 
-Miscellaneous utilities include `eval` for evaluation at a given value of indeterminate,
-and reciprocals `deriv` / `integral`:
+Miscellaneous utilities include `eval` for evaluation at a given point,
+and `deriv` / `integral` for taking the derivative and an indefinite integral, respectively:
 
 ```haskell
 > eval (X^2 + 1 :: UPoly Int) 3
@@ -99,7 +109,7 @@
 
 ## Deconstruction
 
-Use `unPoly` to deconstruct a polynomial to a vector of coefficients (head is the constant term):
+Use `unPoly` to deconstruct a polynomial to a vector of coefficients (constant term first):
 
 ```haskell
 > unPoly (X^2 - 3 * X + 2 :: UPoly Int)
@@ -116,26 +126,34 @@
 
 ## Flavours
 
-The same API is exposed in four flavours:
-
-* `Data.Poly` provides dense polynomials with `Num`-based interface.
+* `Data.Poly` provides dense univariate polynomials with a `Num`-based interface.
   This is a default choice for most users.
 
-* `Data.Poly.Semiring` provides dense polynomials with `Semiring`-based interface.
+* `Data.Poly.Semiring` provides dense univariate polynomials with a `Semiring`-based interface.
 
-* `Data.Poly.Sparse` provides sparse polynomials with `Num`-based interface.
-  Besides that, you may find it easier to use in REPL
+* `Data.Poly.Laurent` provides dense univariate Laurent polynomials with a `Semiring`-based interface.
+
+* `Data.Poly.Sparse` provides sparse univariate polynomials with a `Num`-based interface.
+  Besides that, you may find it easier to use in the REPL
   because of a more readable `Show` instance, skipping zero coefficients.
 
-* `Data.Poly.Sparse.Semiring` provides sparse polynomials with `Semiring`-based interface.
+* `Data.Poly.Sparse.Semiring` provides sparse univariate polynomials with a `Semiring`-based interface.
 
+* `Data.Poly.Sparse.Laurent` provides sparse univariate Laurent polynomials with a `Semiring`-based interface.
+
+* `Data.Poly.Multi` provides sparse multivariate polynomials with a `Num`-based interface.
+
+* `Data.Poly.Multi.Semiring` provides sparse multivariate polynomials with a `Semiring`-based interface.
+
+* `Data.Poly.Multi.Laurent` provides sparse multivariate Laurent polynomials with a `Semiring`-based interface.
+
 All flavours are available backed by boxed or unboxed vectors.
 
 ## Performance
 
-As a rough guide, `poly` is at least 20x-40x faster than [`polynomial`](http://hackage.haskell.org/package/polynomial) library.
-Multiplication is implemented via Karatsuba algorithm.
-Here is a couple of benchmarks for `UPoly Int`.
+As a rough guide, `poly` is at least 20x-40x faster than the [`polynomial`](http://hackage.haskell.org/package/polynomial) library.
+Multiplication is implemented via the Karatsuba algorithm.
+Here are a couple of benchmarks for `UPoly Int`:
 
 | Benchmark                     | polynomial, μs  | poly, μs | speedup
 | :---------------------------- | --------------: | -------: | ------:
@@ -144,3 +162,11 @@
 | addition, 10000 coeffs.       |            6545 |     167  |  39x
 | multiplication, 100 coeffs.   |            1733 |      33  |  52x
 | multiplication, 1000 coeffs.  |          442000 |    1456  | 303x
+
+Due to being polymorphic by multiple axis, the performance of `poly` crucially depends on specialisation of instances. Clients are strongly recommended to compile with `ghc-options: -fspecialise-aggressively` and suggested to enable `-O2`.
+
+## Additional resources
+
+* __Polynomials in Haskell__, MuniHac, 12.09.2020:
+  [slides](https://github.com/Bodigrim/my-talks/raw/master/munihac2020/slides.pdf),
+  [video](https://youtu.be/NAs3ExQZUjA).
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/Bench.hs b/bench/Bench.hs
--- a/bench/Bench.hs
+++ b/bench/Bench.hs
@@ -1,13 +1,18 @@
+{-# LANGUAGE CPP        #-}
 {-# LANGUAGE RankNTypes #-}
 
 module Main where
 
 import Gauge.Main
 import qualified DenseBench as Dense
+#ifdef SupportSparse
 import qualified SparseBench as Sparse
+#endif
 
 main :: IO ()
 main = defaultMain
   [ Dense.benchSuite
+#ifdef SupportSparse
   , Sparse.benchSuite
+#endif
   ]
diff --git a/bench/DenseBench.hs b/bench/DenseBench.hs
--- a/bench/DenseBench.hs
+++ b/bench/DenseBench.hs
@@ -1,5 +1,3 @@
-{-# LANGUAGE CPP                        #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE RankNTypes                 #-}
 {-# LANGUAGE TypeApplications           #-}
 
@@ -9,15 +7,13 @@
 
 import Prelude hiding (quotRem, gcd)
 import Gauge.Main
+import Data.Euclidean (Euclidean(..), GcdDomain(..))
 import Data.Poly
-import qualified Data.Vector.Unboxed as U
-#if MIN_VERSION_semirings(0,5,2)
-import Data.Euclidean (Euclidean(..), GcdDomain(..), Field)
 import qualified Data.Poly.Semiring as S (toPoly)
 import Data.Semiring (Semiring(..), Ring, Mod2(..))
 import qualified Data.Semiring as S (fromIntegral)
 import qualified Data.Vector as V
-#endif
+import qualified Data.Vector.Unboxed as U
 
 benchSuite :: Benchmark
 benchSuite = bgroup "dense" $ concat
@@ -26,14 +22,10 @@
   , map benchEval     [100, 1000, 10000]
   , map benchDeriv    [100, 1000, 10000]
   , map benchIntegral [100, 1000, 10000]
-#if MIN_VERSION_semirings(0,5,2)
-  , map benchQuotRem    [10, 100]
-  , map benchGcd        [10, 100]
-  , map benchGcdExtRat  [10, 20, 40]
-  , map benchGcdFracRat [10, 20, 40]
-  , map benchGcdExtM    [10, 100, 1000]
-  , map benchGcdFracM   [10, 100, 1000]
-#endif
+  , map benchQuotRem  [10, 100]
+  , map benchGcd      [10, 100]
+  , map benchGcdRat   [10, 20, 40]
+  , map benchGcdM     [10, 100, 1000]
   ]
 
 benchAdd :: Int -> Benchmark
@@ -51,27 +43,17 @@
 benchIntegral :: Int -> Benchmark
 benchIntegral k = bench ("integral/" ++ show k) $ nf doIntegral k
 
-#if MIN_VERSION_semirings(0,5,2)
-
 benchQuotRem :: Int -> Benchmark
 benchQuotRem k = bench ("quotRem/" ++ show k) $ nf doQuotRem k
 
 benchGcd :: Int -> Benchmark
-benchGcd k = bench ("gcd/" ++ show k) $ nf doGcd k
-
-benchGcdExtRat :: Int -> Benchmark
-benchGcdExtRat k = bench ("gcdExt/Rational/" ++ show k) $ nf (doGcdExt @Rational) k
-
-benchGcdFracRat :: Int -> Benchmark
-benchGcdFracRat k = bench ("gcdFrac/Rational/" ++ show k) $ nf (doGcdFrac @Rational) k
-
-benchGcdExtM :: Int -> Benchmark
-benchGcdExtM k = bench ("gcdExt/Mod2/" ++ show k) $ nf (getMod2 . doGcdExt @Mod2) k
+benchGcd k = bench ("gcd/Integer/" ++ show k) $ nf (doGcd @Integer) k
 
-benchGcdFracM :: Int -> Benchmark
-benchGcdFracM k = bench ("gcdFrac/Mod2/" ++ show k) $ nf (getMod2 . doGcdFrac @Mod2) k
+benchGcdRat :: Int -> Benchmark
+benchGcdRat k = bench ("gcd/Rational/" ++ show k) $ nf (doGcd @Rational) k
 
-#endif
+benchGcdM :: Int -> Benchmark
+benchGcdM k = bench ("gcd/Mod2/" ++ show k) $ nf (getMod2 . doGcd @Mod2) k
 
 doBinOp :: (forall a. Num a => a -> a -> a) -> Int -> Int
 doBinOp op n = U.sum zs
@@ -98,8 +80,6 @@
     xs = toPoly $ U.generate n ((* 2) . fromIntegral)
     zs = unPoly $ integral xs
 
-#if MIN_VERSION_semirings(0,5,2)
-
 gen1 :: Ring a => Int -> a
 gen1 k = S.fromIntegral (truncate (pi * fromIntegral k :: Double) `mod` (k + 1))
 
@@ -113,25 +93,9 @@
     ys = toPoly $ U.generate n       gen2
     (qs, rs) = xs `quotRem` ys
 
-doGcd :: Int -> Integer
-doGcd n = V.sum gs
-  where
-    xs = toPoly $ V.generate n gen1
-    ys = toPoly $ V.generate n gen2
-    gs = unPoly $ xs `gcd` ys
-
-doGcdExt :: (Eq a, Field a) => Int -> a
-doGcdExt n = V.foldl' plus zero gs
+doGcd :: (Eq a, Ring a, GcdDomain a) => Int -> a
+doGcd n = V.foldl' plus zero gs
   where
     xs = S.toPoly $ V.generate n gen1
     ys = S.toPoly $ V.generate n gen2
-    gs = unPoly $ fst $ xs `gcdExt` ys
-
-doGcdFrac :: (Eq a, Field a) => Int -> a
-doGcdFrac n = V.foldl' plus zero gs
-  where
-    xs = PolyOverField $ S.toPoly $ V.generate n gen1
-    ys = PolyOverField $ S.toPoly $ V.generate n gen2
-    gs = unPoly $ unPolyOverField $ xs `gcd` ys
-
-#endif
+    gs = unPoly $ xs `gcd` ys
diff --git a/bench/SparseBench.hs b/bench/SparseBench.hs
--- a/bench/SparseBench.hs
+++ b/bench/SparseBench.hs
@@ -12,11 +12,12 @@
 
 benchSuite :: Benchmark
 benchSuite = bgroup "sparse" $ concat
-  [ map benchAdd      $ zip3 tabs vecs2 vecs3
-  , map benchMul      $ take 2 $ zip3 tabs vecs2 vecs3
-  , map benchEval     $ zip tabs vecs2
-  , map benchDeriv    $ zip tabs vecs2
-  , map benchIntegral $ zip tabs vecs2'
+  [ zipWith3 benchAdd tabs vecs2 vecs3
+  , take 2
+  $ zipWith3 benchMul tabs vecs2 vecs3
+  , zipWith benchEval tabs vecs2
+  , zipWith benchDeriv tabs vecs2
+  , zipWith benchIntegral tabs vecs2'
   ]
 
 tabs :: [Int]
@@ -34,20 +35,20 @@
 vecs3 = flip map tabs $
   \n -> toPoly $ U.generate n (\i -> (fromIntegral i ^ 3, i * 3))
 
-benchAdd :: (Int, UPoly Int, UPoly Int) -> Benchmark
-benchAdd (k, xs, ys) = bench ("add/" ++ show k) $ nf (doBinOp (+) xs) ys
+benchAdd :: Int -> UPoly Int -> UPoly Int -> Benchmark
+benchAdd k xs ys = bench ("add/" ++ show k) $ nf (doBinOp (+) xs) ys
 
-benchMul :: (Int, UPoly Int, UPoly Int) -> Benchmark
-benchMul (k, xs, ys) = bench ("mul/" ++ show k) $ nf (doBinOp (*) xs) ys
+benchMul :: Int -> UPoly Int -> UPoly Int -> Benchmark
+benchMul k xs ys = bench ("mul/" ++ show k) $ nf (doBinOp (*) xs) ys
 
-benchEval :: (Int, UPoly Int) -> Benchmark
-benchEval (k, xs) = bench ("eval/" ++ show k) $ nf doEval xs
+benchEval :: Int -> UPoly Int -> Benchmark
+benchEval k xs = bench ("eval/" ++ show k) $ nf doEval xs
 
-benchDeriv :: (Int, UPoly Int) -> Benchmark
-benchDeriv (k, xs) = bench ("deriv/" ++ show k) $ nf doDeriv xs
+benchDeriv :: Int -> UPoly Int -> Benchmark
+benchDeriv k xs = bench ("deriv/" ++ show k) $ nf doDeriv xs
 
-benchIntegral :: (Int, UPoly Double) -> Benchmark
-benchIntegral (k, xs) = bench ("integral/" ++ show k) $ nf doIntegral xs
+benchIntegral :: Int -> UPoly Double -> Benchmark
+benchIntegral k xs = bench ("integral/" ++ show k) $ nf doIntegral xs
 
 doBinOp :: (forall a. Num a => a -> a -> a) -> UPoly Int -> UPoly Int -> Int
 doBinOp op xs ys = U.foldl' (\acc (_, x) -> acc + x) 0 zs
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,27 @@
+# 0.5.1.0
+
+* Add function `timesRing`.
+* Tweak inlining pragmas.
+
+# 0.5.0.0
+
+* Change definition of `Data.Euclidean.degree`
+  to coincide with the degree of polynomial.
+* Implement multivariate polynomials (usual and Laurent).
+* Reimplement sparse univariate polynomials as a special case of multivariate ones.
+* Speed up `gcd` calculations for all flavours of polynomials.
+* Decomission `PolyOverField` and `LaurentOverField`: they do not improve performance any more.
+* Add function `quotRemFractional`.
+* Add an experimental implementation of the discrete Fourier transform.
+* Add conversion functions between dense and sparse polynomials.
+
+# 0.4.0.0
+
+* Implement Laurent polynomials.
+* Implement orthogonal polynomials.
+* Decomission extended GCD, use `Data.Euclidean.gcdExt`.
+* Decomission `PolyOverFractional`, use `PolyOverField`.
+
 # 0.3.3.0
 
 * Add function `subst`.
@@ -24,9 +48,13 @@
 
 # 0.2.0.0
 
+* Parametrize `Poly` by underlying vector type.
+* Introduce `Data.Poly.Semiring` module.
 * Fix a bug in `Num.(-)`.
 * Add functions `constant`, `eval`, `deriv`, `integral`.
 * Add a handy pattern synonym `X`.
+* Add type synonyms `VPoly` and `UPoly`.
+* Remove function `toPoly'`.
 
 # 0.1.0.0
 
diff --git a/poly.cabal b/poly.cabal
--- a/poly.cabal
+++ b/poly.cabal
@@ -1,84 +1,144 @@
 name: poly
-version: 0.3.3.0
+version: 0.5.1.0
 synopsis: Polynomials
 description:
-  Polynomials backed by `Vector`.
+  Polynomials backed by `Vector`s.
 homepage: https://github.com/Bodigrim/poly#readme
 license: BSD3
 license-file: LICENSE
 author: Andrew Lelechenko
 maintainer: andrew.lelechenko@gmail.com
-copyright: 2019 Andrew Lelechenko
+copyright: 2019-2020 Andrew Lelechenko
 category: Math, Numerical
 build-type: Simple
-extra-source-files: README.md
-cabal-version: >=1.10
-tested-with: GHC ==8.0.2 GHC ==8.2.2 GHC ==8.4.4 GHC ==8.6.5 GHC ==8.8.1
+cabal-version: 2.0
+tested-with: GHC ==8.6.5 GHC ==8.8.4 GHC ==8.10.7 GHC ==9.0.2 GHC ==9.2.5 GHC ==9.4.4
 extra-source-files:
   changelog.md
+  README.md
 
 source-repository head
   type: git
   location: https://github.com/Bodigrim/poly
 
+flag sparse
+  description:
+    Enable sparse and multivariate polynomials, incurring a larger dependency footprint.
+  default: True
+  manual: True
+
 library
   hs-source-dirs: src
   exposed-modules:
     Data.Poly
+    Data.Poly.Laurent
     Data.Poly.Semiring
-    Data.Poly.Sparse
-    Data.Poly.Sparse.Semiring
+    Data.Poly.Orthogonal
+
+  if flag(sparse)
+    exposed-modules:
+      Data.Poly.Sparse
+      Data.Poly.Sparse.Laurent
+      Data.Poly.Sparse.Semiring
+
+      Data.Poly.Multi
+      Data.Poly.Multi.Laurent
+      Data.Poly.Multi.Semiring
+
   other-modules:
     Data.Poly.Internal.Dense
     Data.Poly.Internal.Dense.Field
+    Data.Poly.Internal.Dense.DFT
     Data.Poly.Internal.Dense.GcdDomain
-    Data.Poly.Internal.PolyOverField
-    Data.Poly.Internal.Sparse
-    Data.Poly.Internal.Sparse.Field
-    Data.Poly.Internal.Sparse.GcdDomain
+    Data.Poly.Internal.Dense.Laurent
+
+  if flag(sparse)
+    other-modules:
+      Data.Poly.Internal.Convert
+      Data.Poly.Internal.Multi
+      Data.Poly.Internal.Multi.Core
+      Data.Poly.Internal.Multi.Field
+      Data.Poly.Internal.Multi.GcdDomain
+      Data.Poly.Internal.Multi.Laurent
+
   build-depends:
-    base >= 4.9 && < 5,
+    base >= 4.12 && < 5,
     deepseq >= 1.1 && < 1.5,
     primitive >= 0.6,
-    semirings >= 0.2,
-    vector >= 0.12.0.2,
-    vector-algorithms >= 0.7
+    semirings >= 0.5.2,
+    vector >= 0.12.0.2
+
+  if flag(sparse)
+    build-depends:
+      finite-typelits >= 0.1,
+      vector-algorithms >= 0.8.0.3,
+      vector-sized >= 1.1
+
   default-language: Haskell2010
-  ghc-options: -Wall
+  other-extensions: QuantifiedConstraints
+  ghc-options: -Wall -Wcompat -Wredundant-constraints
 
+  if flag(sparse)
+    cpp-options: -DSupportSparse
+
 test-suite poly-tests
   type: exitcode-stdio-1.0
   main-is: Main.hs
   other-modules:
     Dense
+    DenseLaurent
+    DFT
+    Orthogonal
     Quaternion
-    Sparse
+    TestUtils
+  if flag(sparse)
+    other-modules:
+      Multi
+      MultiLaurent
+      Sparse
+      SparseLaurent
   build-depends:
-    base >=4.9 && <5,
+    base >=4.10 && <5,
+    mod >=0.1.2,
     poly,
     QuickCheck >=2.12,
-    quickcheck-classes >=0.5,
-    semirings >= 0.2,
+    quickcheck-classes-base,
+    quickcheck-classes >=0.6.3,
+    semirings >= 0.5.2,
     tasty >= 0.11,
     tasty-quickcheck >= 0.8,
     vector >= 0.12.0.2
+  if flag(sparse)
+    build-depends:
+      finite-typelits,
+      vector-sized >= 1.4.2
   default-language: Haskell2010
   hs-source-dirs: test
-  ghc-options: -Wall
+  ghc-options: -Wall -Wcompat -threaded -rtsopts
+  if flag(sparse)
+    cpp-options: -DSupportSparse
 
-benchmark poly-gauge
+benchmark poly-bench
   build-depends:
-    base >=4.9 && <5,
+    base >=4.10 && <5,
     deepseq >= 1.1 && < 1.5,
-    gauge >= 0.1,
+    mod >=0.1.2,
     poly,
     semirings >= 0.2,
     vector >= 0.12.0.2
+  build-depends:
+    tasty-bench
+  mixins:
+    tasty-bench (Test.Tasty.Bench as Gauge.Main)
   type: exitcode-stdio-1.0
   main-is: Bench.hs
   other-modules:
     DenseBench
-    SparseBench
+  if flag(sparse)
+    other-modules:
+      SparseBench
   default-language: Haskell2010
   hs-source-dirs: bench
-  ghc-options: -Wall
+  ghc-options: -Wall -Wcompat -O2 -fspecialise-aggressively
+  if flag(sparse)
+    cpp-options: -DSupportSparse
diff --git a/src/Data/Poly.hs b/src/Data/Poly.hs
--- a/src/Data/Poly.hs
+++ b/src/Data/Poly.hs
@@ -6,6 +6,8 @@
 --
 -- Dense polynomials and a 'Num'-based interface.
 --
+-- @since 0.1.0.0
+--
 
 {-# LANGUAGE CPP             #-}
 {-# LANGUAGE PatternSynonyms #-}
@@ -16,7 +18,6 @@
   , UPoly
   , unPoly
   , leading
-  -- * Num interface
   , toPoly
   , monomial
   , scale
@@ -25,19 +26,16 @@
   , subst
   , deriv
   , integral
-#if MIN_VERSION_semirings(0,4,2)
-  -- * Polynomials over 'Field'
-  , PolyOverField(..)
-  , gcdExt
-  , PolyOverFractional
-  , pattern PolyOverFractional
-  , unPolyOverFractional
+  , quotRemFractional
+#ifdef SupportSparse
+  , denseToSparse
+  , sparseToDense
 #endif
   ) where
 
+#ifdef SupportSparse
+import Data.Poly.Internal.Convert
+#endif
 import Data.Poly.Internal.Dense
-#if MIN_VERSION_semirings(0,4,2)
-import Data.Poly.Internal.Dense.Field (gcdExt)
+import Data.Poly.Internal.Dense.Field (quotRemFractional)
 import Data.Poly.Internal.Dense.GcdDomain ()
-import Data.Poly.Internal.PolyOverField
-#endif
diff --git a/src/Data/Poly/Internal/Convert.hs b/src/Data/Poly/Internal/Convert.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Convert.hs
@@ -0,0 +1,92 @@
+-- |
+-- Module:      Data.Poly.Internal.Convert
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Conversions between polynomials.
+--
+
+{-# LANGUAGE DataKinds        #-}
+{-# LANGUAGE FlexibleContexts #-}
+
+module Data.Poly.Internal.Convert
+  ( denseToSparse
+  , denseToSparse'
+  , sparseToDense
+  , sparseToDense'
+  ) where
+
+import Control.Monad.ST
+import Data.Semiring (Semiring(..))
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Mutable as MG
+import qualified Data.Vector.Unboxed.Sized as SU
+
+import qualified Data.Poly.Internal.Dense as Dense
+import qualified Data.Poly.Internal.Multi as Sparse
+
+-- | Convert from dense to sparse polynomials.
+--
+-- >>> :set -XFlexibleContexts
+-- >>> denseToSparse (1 + Data.Poly.X^2) :: Data.Poly.Sparse.UPoly Int
+-- 1 * X^2 + 1
+--
+-- @since 0.5.0.0
+denseToSparse
+  :: (Eq a, Num a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => Dense.Poly v a
+  -> Sparse.Poly v a
+denseToSparse = denseToSparseInternal 0
+
+denseToSparse'
+  :: (Eq a, Semiring a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => Dense.Poly v a
+  -> Sparse.Poly v a
+denseToSparse' = denseToSparseInternal zero
+
+denseToSparseInternal
+  :: (Eq a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => a
+  -> Dense.Poly v a
+  -> Sparse.Poly v a
+denseToSparseInternal z = Sparse.MultiPoly . G.imapMaybe (\i c -> if c == z then Nothing else Just (fromIntegral i, c)) . Dense.unPoly
+
+-- | Convert from sparse to dense polynomials.
+--
+-- >>> :set -XFlexibleContexts
+-- >>> sparseToDense (1 + Data.Poly.Sparse.X^2) :: Data.Poly.UPoly Int
+-- 1 * X^2 + 0 * X + 1
+--
+-- @since 0.5.0.0
+sparseToDense
+  :: (Num a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => Sparse.Poly v a
+  -> Dense.Poly v a
+sparseToDense = sparseToDenseInternal 0
+
+sparseToDense'
+  :: (Semiring a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => Sparse.Poly v a
+  -> Dense.Poly v a
+sparseToDense' = sparseToDenseInternal zero
+
+sparseToDenseInternal
+  :: (G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => a
+  -> Sparse.Poly v a
+  -> Dense.Poly v a
+sparseToDenseInternal z (Sparse.MultiPoly xs)
+  | G.null xs = Dense.Poly G.empty
+  | otherwise = runST $ do
+  let len = fromIntegral (SU.head (fst (G.unsafeLast xs)) + 1)
+  ys <- MG.unsafeNew len
+  MG.set ys z
+  let go xi yi
+        | xi >= G.length xs = pure ()
+        | (yi', c) <- G.unsafeIndex xs xi
+        , yi == fromIntegral (SU.head yi')
+        = MG.unsafeWrite ys yi c >> go (xi + 1) (yi + 1)
+        | otherwise = go xi (yi + 1)
+  go 0 0
+  Dense.Poly <$> G.unsafeFreeze ys
diff --git a/src/Data/Poly/Internal/Dense.hs b/src/Data/Poly/Internal/Dense.hs
--- a/src/Data/Poly/Internal/Dense.hs
+++ b/src/Data/Poly/Internal/Dense.hs
@@ -7,11 +7,11 @@
 -- Dense polynomials of one variable.
 --
 
-{-# LANGUAGE CPP                        #-}
 {-# LANGUAGE FlexibleInstances          #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE PatternSynonyms            #-}
 {-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeApplications           #-}
 {-# LANGUAGE TypeFamilies               #-}
 {-# LANGUAGE ViewPatterns               #-}
 
@@ -37,39 +37,33 @@
   , pattern X'
   , eval'
   , subst'
+  , substitute'
   , deriv'
-#if MIN_VERSION_semirings(0,5,0)
   , unscale'
   , integral'
-#endif
+  , timesRing
   ) where
 
-import Prelude hiding (quotRem, quot, rem, gcd, lcm, (^))
+import Prelude hiding (quotRem, quot, rem, gcd, lcm)
 import Control.DeepSeq (NFData)
 import Control.Monad
-import Control.Monad.Primitive
 import Control.Monad.ST
 import Data.Bits
+import Data.Euclidean (Euclidean, Field, quot)
+import Data.Kind
 import Data.List (foldl', intersperse)
-import Data.Semiring (Semiring(..), Ring())
+import Data.Semiring (Semiring(..), Ring(), minus)
 import qualified Data.Semiring as Semiring
 import qualified Data.Vector as V
 import qualified Data.Vector.Generic as G
 import qualified Data.Vector.Generic.Mutable as MG
 import qualified Data.Vector.Unboxed as U
 import GHC.Exts
-#if !MIN_VERSION_semirings(0,4,0)
-import Data.Semigroup
-import Numeric.Natural
-#endif
-#if MIN_VERSION_semirings(0,5,0)
-import Data.Euclidean (Euclidean, Field, quot)
-#endif
 
 -- | Polynomials of one variable with coefficients from @a@,
 -- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
 --
--- Use pattern 'X' for construction:
+-- Use the pattern 'X' for construction:
 --
 -- >>> (X + 1) + (X - 1) :: VPoly Integer
 -- 2 * X + 0
@@ -79,16 +73,27 @@
 -- Polynomials are stored normalized, without leading
 -- zero coefficients, so 0 * 'X' + 1 equals to 1.
 --
--- 'Ord' instance does not make much sense mathematically,
+-- The 'Ord' instance does not make much sense mathematically,
 -- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
 --
-newtype Poly v a = Poly
+-- Due to being polymorphic by multiple axis, the performance of `Poly` crucially
+-- depends on specialisation of instances. Clients are strongly recommended
+-- to compile with @ghc-options:@ @-fspecialise-aggressively@ and suggested to enable @-O2@.
+--
+-- @since 0.1.0.0
+newtype Poly (v :: Type -> Type) (a :: Type) = Poly
   { unPoly :: v a
-  -- ^ Convert 'Poly' to a vector of coefficients
-  -- (first element corresponds to a constant term).
+  -- ^ Convert a 'Poly' to a vector of coefficients
+  -- (first element corresponds to the constant term).
+  --
+  -- @since 0.1.0.0
   }
-  deriving (Eq, NFData, Ord)
+  deriving
+  ( Eq, Ord
+  , NFData -- ^ @since 0.3.2.0
+  )
 
+-- | @since 0.3.1.0
 instance (Eq a, Semiring a, G.Vector v a) => IsList (Poly v a) where
   type Item (Poly v a) = a
   fromList = toPoly' . G.fromList
@@ -107,81 +112,105 @@
       $ intersperse (showString " + ")
       $ G.ifoldl (\acc i c -> showCoeff i c : acc) [] xs
     where
+      -- Powers are guaranteed to be non-negative
+      showCoeff :: Int -> a -> String -> String
       showCoeff 0 c = showsPrec 7 c
       showCoeff 1 c = showsPrec 7 c . showString " * X"
-      showCoeff i c = showsPrec 7 c . showString " * X^" . showsPrec 7 i
+      showCoeff i c = showsPrec 7 c . showString (" * X^" ++ show i)
 
 -- | Polynomials backed by boxed vectors.
+--
+-- @since 0.2.0.0
 type VPoly = Poly V.Vector
 
 -- | Polynomials backed by unboxed vectors.
+--
+-- @since 0.2.0.0
 type UPoly = Poly U.Vector
 
--- | Make 'Poly' from a list of coefficients
--- (first element corresponds to a constant term).
+-- | Make a 'Poly' from a list of coefficients
+-- (first element corresponds to the constant term).
 --
 -- >>> :set -XOverloadedLists
 -- >>> toPoly [1,2,3] :: VPoly Integer
 -- 3 * X^2 + 2 * X + 1
 -- >>> toPoly [0,0,0] :: UPoly Int
 -- 0
+--
+-- @since 0.1.0.0
 toPoly :: (Eq a, Num a, G.Vector v a) => v a -> Poly v a
 toPoly = Poly . dropWhileEnd (== 0)
+{-# INLINABLE toPoly #-}
 
 toPoly' :: (Eq a, Semiring a, G.Vector v a) => v a -> Poly v a
 toPoly' = Poly . dropWhileEnd (== zero)
+{-# INLINABLE toPoly' #-}
 
--- | Return a leading power and coefficient of a non-zero polynomial.
+-- | Return the leading power and coefficient of a non-zero polynomial.
 --
 -- >>> leading ((2 * X + 1) * (2 * X^2 - 1) :: UPoly Int)
 -- Just (3,4)
 -- >>> leading (0 :: UPoly Int)
 -- Nothing
+--
+-- @since 0.3.0.0
 leading :: G.Vector v a => Poly v a -> Maybe (Word, a)
 leading (Poly v)
   | G.null v  = Nothing
   | otherwise = Just (fromIntegral (G.length v - 1), G.last v)
+{-# INLINABLE leading #-}
 
 -- | Note that 'abs' = 'id' and 'signum' = 'const' 1.
 instance (Eq a, Num a, G.Vector v a) => Num (Poly v a) where
-  Poly xs + Poly ys = toPoly $ plusPoly (+) xs ys
-  Poly xs - Poly ys = toPoly $ minusPoly negate (-) xs ys
+  (+) = (toPoly .) . coerce (plusPoly @v @a (+))
+  (-) = (toPoly .) . coerce (minusPoly @v @a negate (-))
+  (*) = (toPoly .) . coerce (inline (karatsuba @v @a 0 (+) (-) (*)))
+
   negate (Poly xs) = Poly $ G.map negate xs
   abs = id
   signum = const 1
   fromInteger n = case fromInteger n of
     0 -> Poly G.empty
     m -> Poly $ G.singleton m
-  Poly xs * Poly ys = toPoly $ karatsuba xs ys
+
   {-# INLINE (+) #-}
   {-# INLINE (-) #-}
   {-# INLINE negate #-}
   {-# INLINE fromInteger #-}
   {-# INLINE (*) #-}
 
+-- | Note that 'times' is significantly slower than '(*)' for large polynomials,
+-- because Karatsuba multiplication algorithm requires subtraction, which is not
+-- provided by 'Semiring' class. Use 'timesRing' instead.
 instance (Eq a, Semiring a, G.Vector v a) => Semiring (Poly v a) where
   zero = Poly G.empty
   one
     | (one :: a) == zero = zero
     | otherwise = Poly $ G.singleton one
-  plus (Poly xs) (Poly ys) = toPoly' $ plusPoly plus xs ys
-  times (Poly xs) (Poly ys) = toPoly' $ convolution zero plus times xs ys
+
+  plus  = (toPoly' .) . coerce (plusPoly @v @a plus)
+  times = (toPoly' .) . coerce (inline (convolution @v @a zero plus times))
+
   {-# INLINE zero #-}
   {-# INLINE one #-}
   {-# INLINE plus #-}
   {-# INLINE times #-}
 
-#if MIN_VERSION_semirings(0,4,0)
   fromNatural n = if n' == zero then zero else Poly $ G.singleton n'
     where
       n' :: a
       n' = fromNatural n
   {-# INLINE fromNatural #-}
-#endif
 
 instance (Eq a, Ring a, G.Vector v a) => Ring (Poly v a) where
   negate (Poly xs) = Poly $ G.map Semiring.negate xs
+  {-# INLINABLE negate #-}
 
+-- | Karatsuba multiplication algorithm for polynomials over rings.
+timesRing :: forall v a. (Eq a, Ring a, G.Vector v a) => Poly v a -> Poly v a -> Poly v a
+timesRing = (toPoly' .) . coerce (inline (karatsuba @v @a zero plus minus times))
+{-# INLINE timesRing #-}
+
 dropWhileEnd
   :: G.Vector v a
   => (a -> Bool)
@@ -194,10 +223,10 @@
 {-# INLINE dropWhileEnd #-}
 
 dropWhileEndM
-  :: (PrimMonad m, G.Vector v a)
+  :: G.Vector v a
   => (a -> Bool)
-  -> G.Mutable v (PrimState m) a
-  -> m (G.Mutable v (PrimState m) a)
+  -> G.Mutable v s a
+  -> ST s (G.Mutable v s a)
 dropWhileEndM p xs = go (MG.length xs)
   where
     go 0 = pure $ MG.unsafeSlice 0 0 xs
@@ -259,50 +288,57 @@
 karatsubaThreshold = 32
 
 karatsuba
-  :: (Eq a, Num a, G.Vector v a)
-  => v a
+  :: G.Vector v a
+  => a
+  -> (a -> a -> a)
+  -> (a -> a -> a)
+  -> (a -> a -> a)
   -> v a
   -> v a
-karatsuba xs ys
-  | lenXs <= karatsubaThreshold || lenYs <= karatsubaThreshold
-  = convolution 0 (+) (*) xs ys
-  | otherwise = runST $ do
-    zs <- MG.unsafeNew lenZs
-    forM_ [0 .. lenZs - 1] $ \k -> do
-      let z0 = if k < G.length zs0
-               then G.unsafeIndex zs0 k
-               else 0
-          z11 = if k - m >= 0 && k - m < G.length zs11
-               then G.unsafeIndex zs11 (k - m)
-               else 0
-          z10 = if k - m >= 0 && k - m < G.length zs0
-               then G.unsafeIndex zs0 (k - m)
-               else 0
-          z12 = if k - m >= 0 && k - m < G.length zs2
-               then G.unsafeIndex zs2 (k - m)
-               else 0
-          z2 = if k - 2 * m >= 0 && k - 2 * m < G.length zs2
-               then G.unsafeIndex zs2 (k - 2 * m)
-               else 0
-      MG.unsafeWrite zs k (z0 + (z11 - z10 - z12) + z2)
-    G.unsafeFreeze zs
+  -> v a
+karatsuba zer add sub mul = go
   where
-    lenXs = G.length xs
-    lenYs = G.length ys
-    lenZs = lenXs + lenYs - 1
+    conv = inline convolution zer add mul
+    go xs ys
+      | lenXs <= karatsubaThreshold || lenYs <= karatsubaThreshold
+      = conv xs ys
+      | otherwise = runST $ do
+        zs <- MG.unsafeNew lenZs
+        forM_ [0 .. lenZs - 1] $ \k -> do
+          let z0 = if k < G.length zs0
+                   then G.unsafeIndex zs0 k
+                   else zer
+              z11 = if k - m >= 0 && k - m < G.length zs11
+                   then G.unsafeIndex zs11 (k - m)
+                   else zer
+              z10 = if k - m >= 0 && k - m < G.length zs0
+                   then G.unsafeIndex zs0 (k - m)
+                   else zer
+              z12 = if k - m >= 0 && k - m < G.length zs2
+                   then G.unsafeIndex zs2 (k - m)
+                   else zer
+              z2 = if k - 2 * m >= 0 && k - 2 * m < G.length zs2
+                   then G.unsafeIndex zs2 (k - 2 * m)
+                   else zer
+          MG.unsafeWrite zs k (z0 `add` (z11 `sub` (z10 `add` z12)) `add` z2)
+        G.unsafeFreeze zs
+      where
+        lenXs = G.length xs
+        lenYs = G.length ys
+        lenZs = lenXs + lenYs - 1
 
-    m    = ((lenXs `min` lenYs) + 1) `shiftR` 1
+        m    = ((lenXs `min` lenYs) + 1) `shiftR` 1
 
-    xs0  = G.slice 0 m xs
-    xs1  = G.slice m (lenXs - m) xs
-    ys0  = G.slice 0 m ys
-    ys1  = G.slice m (lenYs - m) ys
+        xs0  = G.slice 0 m xs
+        xs1  = G.slice m (lenXs - m) xs
+        ys0  = G.slice 0 m ys
+        ys1  = G.slice m (lenYs - m) ys
 
-    xs01 = plusPoly (+) xs0 xs1
-    ys01 = plusPoly (+) ys0 ys1
-    zs0  = karatsuba xs0 ys0
-    zs2  = karatsuba xs1 ys1
-    zs11 = karatsuba xs01 ys01
+        xs01 = plusPoly add xs0 xs1
+        ys01 = plusPoly add ys0 ys1
+        zs0  = go xs0 ys0
+        zs2  = go xs1 ys1
+        zs11 = go xs01 ys01
 {-# INLINABLE karatsuba #-}
 
 convolution
@@ -313,19 +349,21 @@
   -> v a
   -> v a
   -> v a
-convolution zer add mul xs ys
-  | lenXs == 0 || lenYs == 0 = G.empty
-  | otherwise = G.generate lenZs $ \k -> foldl'
+convolution zer add mul = \xs ys ->
+  let lenXs = G.length xs
+      lenYs = G.length ys
+      lenZs = lenXs + lenYs - 1 in
+  if lenXs == 0 || lenYs == 0
+  then G.empty
+  else G.generate lenZs $ \k -> foldl'
     (\acc i -> acc `add` mul (G.unsafeIndex xs i) (G.unsafeIndex ys (k - i)))
     zer
     [max (k - lenYs + 1) 0 .. min k (lenXs - 1)]
-  where
-    lenXs = G.length xs
-    lenYs = G.length ys
-    lenZs = lenXs + lenYs - 1
 {-# INLINABLE convolution #-}
 
 -- | Create a monomial from a power and a coefficient.
+--
+-- @since 0.3.0.0
 monomial :: (Eq a, Num a, G.Vector v a) => Word -> a -> Poly v a
 monomial _ 0 = Poly G.empty
 monomial p c = Poly $ G.generate (fromIntegral p + 1) (\i -> if i == fromIntegral p then c else 0)
@@ -338,7 +376,7 @@
 {-# INLINE monomial' #-}
 
 scaleInternal
-  :: (Eq a, G.Vector v a)
+  :: G.Vector v a
   => a
   -> (a -> a -> a)
   -> Word
@@ -359,13 +397,14 @@
 --
 -- >>> scale 2 3 (X^2 + 1) :: UPoly Int
 -- 3 * X^4 + 0 * X^3 + 3 * X^2 + 0 * X + 0
+--
+-- @since 0.3.0.0
 scale :: (Eq a, Num a, G.Vector v a) => Word -> a -> Poly v a -> Poly v a
 scale yp yc (Poly xs) = toPoly $ scaleInternal 0 (*) yp yc xs
 
 scale' :: (Eq a, Semiring a, G.Vector v a) => Word -> a -> Poly v a -> Poly v a
 scale' yp yc (Poly xs) = toPoly' $ scaleInternal zero times yp yc xs
 
-#if MIN_VERSION_semirings(0,5,0)
 unscale' :: (Eq a, Euclidean a, G.Vector v a) => Word -> a -> Poly v a -> Poly v a
 unscale' yp yc (Poly xs) = toPoly' $ runST $ do
   let lenZs = G.length xs - fromIntegral yp
@@ -374,7 +413,6 @@
     MG.unsafeWrite zs k (G.unsafeIndex xs (k + fromIntegral yp) `quot` yc)
   G.unsafeFreeze zs
 {-# INLINABLE unscale' #-}
-#endif
 
 data StrictPair a b = !a :*: !b
 
@@ -383,10 +421,12 @@
 fst' :: StrictPair a b -> a
 fst' (a :*: _) = a
 
--- | Evaluate at a given point.
+-- | Evaluate the polynomial at a given point.
 --
 -- >>> eval (X^2 + 1 :: UPoly Int) 3
 -- 10
+--
+-- @since 0.2.0.0
 eval :: (Num a, G.Vector v a) => Poly v a -> a -> a
 eval = substitute (*)
 {-# INLINE eval #-}
@@ -399,6 +439,8 @@
 --
 -- >>> subst (X^2 + 1 :: UPoly Int) (X + 1 :: UPoly Int)
 -- 1 * X^2 + 2 * X + 2
+--
+-- @since 0.3.3.0
 subst :: (Eq a, Num a, G.Vector v a, G.Vector w a) => Poly v a -> Poly w a -> Poly w a
 subst = substitute (scale 0)
 {-# INLINE subst #-}
@@ -417,10 +459,12 @@
   G.foldl' (\(acc :*: xn) cn -> acc `plus` f cn xn :*: x `times` xn) (zero :*: one) cs
 {-# INLINE substitute' #-}
 
--- | Take a derivative.
+-- | Take the derivative of the polynomial.
 --
 -- >>> deriv (X^3 + 3 * X) :: UPoly Int
 -- 3 * X^2 + 0 * X + 3
+--
+-- @since 0.2.0.0
 deriv :: (Eq a, Num a, G.Vector v a) => Poly v a -> Poly v a
 deriv (Poly xs)
   | G.null xs = Poly G.empty
@@ -433,22 +477,13 @@
   | otherwise = toPoly' $ G.imap (\i x -> fromNatural (fromIntegral (i + 1)) `times` x) $ G.tail xs
 {-# INLINE deriv' #-}
 
-#if !MIN_VERSION_semirings(0,4,0)
-fromNatural :: Semiring a => Natural -> a
-fromNatural 0 = zero
-fromNatural n = getAdd' (stimes n (Add' one))
-
-newtype Add' a = Add' { getAdd' :: a }
-
-instance Semiring a => Semigroup (Add' a) where
-  Add' a <> Add' b = Add' (a `plus` b)
-#endif
-
--- | Compute an indefinite integral of a polynomial,
--- setting constant term to zero.
+-- | Compute an indefinite integral of the polynomial,
+-- setting the constant term to zero.
 --
 -- >>> integral (3 * X^2 + 3) :: UPoly Double
 -- 1.0 * X^3 + 0.0 * X^2 + 3.0 * X + 0.0
+--
+-- @since 0.2.0.0
 integral :: (Eq a, Fractional a, G.Vector v a) => Poly v a -> Poly v a
 integral (Poly xs)
   | G.null xs = Poly G.empty
@@ -462,7 +497,6 @@
       lenXs = G.length xs
 {-# INLINABLE integral #-}
 
-#if MIN_VERSION_semirings(0,5,0)
 integral' :: (Eq a, Field a, G.Vector v a) => Poly v a -> Poly v a
 integral' (Poly xs)
   | G.null xs = Poly G.empty
@@ -475,26 +509,41 @@
     where
       lenXs = G.length xs
 {-# INLINABLE integral' #-}
-#endif
 
--- | Create an identity polynomial.
-pattern X :: (Eq a, Num a, G.Vector v a, Eq (v a)) => Poly v a
-pattern X <- ((==) var -> True)
+-- | The polynomial 'X'.
+--
+-- > X == monomial 1 1
+--
+-- @since 0.2.0.0
+pattern X :: (Eq a, Num a, G.Vector v a) => Poly v a
+pattern X <- (isVar -> True)
   where X = var
 
-var :: forall a v. (Eq a, Num a, G.Vector v a, Eq (v a)) => Poly v a
+var :: forall a v. (Eq a, Num a, G.Vector v a) => Poly v a
 var
   | (1 :: a) == 0 = Poly G.empty
   | otherwise     = Poly $ G.fromList [0, 1]
 {-# INLINE var #-}
 
+isVar :: forall v a. (Eq a, Num a, G.Vector v a) => Poly v a -> Bool
+isVar (Poly xs)
+  | (1 :: a) == 0 = G.null xs
+  | otherwise     = G.length xs == 2 && xs G.! 0 == 0 && xs G.! 1 == 1
+{-# INLINE isVar #-}
+
 -- | Create an identity polynomial.
-pattern X' :: (Eq a, Semiring a, G.Vector v a, Eq (v a)) => Poly v a
-pattern X' <- ((==) var' -> True)
+pattern X' :: (Eq a, Semiring a, G.Vector v a) => Poly v a
+pattern X' <- (isVar' -> True)
   where X' = var'
 
-var' :: forall a v. (Eq a, Semiring a, G.Vector v a, Eq (v a)) => Poly v a
+var' :: forall a v. (Eq a, Semiring a, G.Vector v a) => Poly v a
 var'
   | (one :: a) == zero = Poly G.empty
   | otherwise          = Poly $ G.fromList [zero, one]
 {-# INLINE var' #-}
+
+isVar' :: forall v a. (Eq a, Semiring a, G.Vector v a) => Poly v a -> Bool
+isVar' (Poly xs)
+  | (one :: a) == zero = G.null xs
+  | otherwise          = G.length xs == 2 && xs G.! 0 == zero && xs G.! 1 == one
+{-# INLINE isVar' #-}
diff --git a/src/Data/Poly/Internal/Dense/DFT.hs b/src/Data/Poly/Internal/Dense/DFT.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Dense/DFT.hs
@@ -0,0 +1,86 @@
+-- |
+-- Module:      Data.Poly.Internal.Dense.FFT
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Discrete Fourier transform.
+--
+
+{-# LANGUAGE BangPatterns        #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Data.Poly.Internal.Dense.DFT
+  ( dft
+  , inverseDft
+  ) where
+
+import Prelude hiding (recip, fromIntegral)
+import Control.Monad.ST
+import Data.Bits hiding (shift)
+import Data.Foldable
+import Data.Semiring (Semiring(..), Ring(..), minus, fromIntegral)
+import Data.Field (Field, recip)
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Mutable as MG
+
+-- | <https://en.wikipedia.org/wiki/Fast_Fourier_transform Discrete Fourier transform>
+-- \( y_k = \sum_{j=0}^{N-1} x_j \sqrt[N]{1}^{jk} \).
+--
+-- @since 0.5.0.0
+dft
+  :: (Ring a, G.Vector v a)
+  => a   -- ^ primitive root \( \sqrt[N]{1} \), otherwise behaviour is undefined
+  -> v a -- ^ \( \{ x_k \}_{k=0}^{N-1} \) (currently only  \( N = 2^n \) is supported)
+  -> v a -- ^ \( \{ y_k \}_{k=0}^{N-1} \)
+dft primRoot (xs :: v a)
+  | popCount nn /= 1 = error "dft: only vectors of length 2^n are supported"
+  | otherwise = go 0 0
+  where
+    nn = G.length xs
+    n = countTrailingZeros nn
+
+    roots :: v a
+    roots = G.iterateN
+      (1 `unsafeShiftL` (n - 1))
+      (\x -> x `seq` (x `times` primRoot))
+      one
+
+    go !offset !shift
+      | shift >= n = G.unsafeSlice offset 1 xs
+      | otherwise = runST $ do
+        let halfLen = 1 `unsafeShiftL` (n - shift - 1)
+            ys0 = go offset (shift + 1)
+            ys1 = go (offset + 1 `unsafeShiftL` shift) (shift + 1)
+        ys <- MG.new (halfLen `unsafeShiftL` 1)
+
+        -- This corresponds to k = 0 in the loop below.
+        -- It improves performance by avoiding multiplication
+        -- by roots V.! 0 = 1.
+        let y00 = G.unsafeIndex ys0 0
+            y10 = G.unsafeIndex ys1 0
+        MG.unsafeWrite ys 0       $! y00 `plus`  y10
+        MG.unsafeWrite ys halfLen $! y00 `minus` y10
+
+        forM_ [1..halfLen - 1] $ \k -> do
+          let y0 = G.unsafeIndex ys0 k
+              y1 = G.unsafeIndex ys1 k `times`
+                   G.unsafeIndex roots (k `unsafeShiftL` shift)
+          MG.unsafeWrite ys k             $! y0 `plus`  y1
+          MG.unsafeWrite ys (k + halfLen) $! y0 `minus` y1
+        G.unsafeFreeze ys
+{-# INLINABLE dft #-}
+
+-- | Inverse <https://en.wikipedia.org/wiki/Fast_Fourier_transform discrete Fourier transform>
+-- \( x_k = {1\over N} \sum_{j=0}^{N-1} y_j \sqrt[N]{1}^{-jk} \).
+--
+-- @since 0.5.0.0
+inverseDft
+  :: (Field a, G.Vector v a)
+  => a   -- ^ primitive root \( \sqrt[N]{1} \), otherwise behaviour is undefined
+  -> v a -- ^ \( \{ y_k \}_{k=0}^{N-1} \) (currently only  \( N = 2^n \) is supported)
+  -> v a -- ^ \( \{ x_k \}_{k=0}^{N-1} \)
+inverseDft primRoot ys = G.map (`times` invN) $ dft (recip primRoot) ys
+  where
+    invN = recip $ fromIntegral $ G.length ys
+{-# INLINABLE inverseDft #-}
diff --git a/src/Data/Poly/Internal/Dense/Field.hs b/src/Data/Poly/Internal/Dense/Field.hs
--- a/src/Data/Poly/Internal/Dense/Field.hs
+++ b/src/Data/Poly/Internal/Dense/Field.hs
@@ -4,36 +4,25 @@
 -- Licence:     BSD3
 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
 --
--- GcdDomain for Field underlying
+-- 'Euclidean' instance with a 'Field' constraint on the coefficient type.
 --
 
 {-# LANGUAGE ConstraintKinds            #-}
-{-# LANGUAGE CPP                        #-}
 {-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE PatternSynonyms            #-}
 {-# LANGUAGE ScopedTypeVariables        #-}
 {-# LANGUAGE TypeFamilies               #-}
 
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
-#if MIN_VERSION_semirings(0,4,2)
-
 module Data.Poly.Internal.Dense.Field
-  ( fieldGcd
-  , gcdExt
+  ( quotRemFractional
   ) where
 
 import Prelude hiding (quotRem, quot, rem, gcd)
 import Control.Exception
 import Control.Monad
-import Control.Monad.Primitive
 import Control.Monad.ST
-import Data.Euclidean (Euclidean(..))
-#if !MIN_VERSION_semirings(0,5,0)
-import Data.Semiring (Ring)
-#else
-import Data.Euclidean (Field)
-#endif
+import Data.Euclidean (Euclidean(..), Field)
 import Data.Semiring (times, minus, zero, one)
 import qualified Data.Vector.Generic as G
 import qualified Data.Vector.Generic.Mutable as MG
@@ -41,48 +30,74 @@
 import Data.Poly.Internal.Dense
 import Data.Poly.Internal.Dense.GcdDomain ()
 
-#if !MIN_VERSION_semirings(0,5,0)
-type Field a = (Euclidean a, Ring a, Fractional a)
-#endif
-
-instance (Eq a, Eq (v a), Field a, G.Vector v a) => Euclidean (Poly v a) where
-  degree (Poly xs) = fromIntegral (G.length xs)
+-- | Note that 'degree' 0 = 0.
+--
+-- @since 0.3.0.0
+instance (Eq a, Field a, G.Vector v a) => Euclidean (Poly v a) where
+  degree (Poly xs)
+    | G.null xs = 0
+    | otherwise = fromIntegral (G.length xs - 1)
 
   quotRem (Poly xs) (Poly ys) = (toPoly' qs, toPoly' rs)
     where
-      (qs, rs) = quotientAndRemainder xs ys
+      (qs, rs) = quotientAndRemainder zero (== one) minus times (one `quot`) xs ys
   {-# INLINE quotRem #-}
 
   rem (Poly xs) (Poly ys) = toPoly' $ remainder xs ys
   {-# INLINE rem #-}
 
+-- | Polynomial division with remainder.
+--
+-- >>> quotRemFractional (X^3 + 2) (X^2 - 1 :: UPoly Double)
+-- (1.0 * X + 0.0,1.0 * X + 2.0)
+--
+-- @since 0.5.0.0
+quotRemFractional :: (Eq a, Fractional a, G.Vector v a) => Poly v a -> Poly v a -> (Poly v a, Poly v a)
+quotRemFractional (Poly xs) (Poly ys) = (toPoly qs, toPoly rs)
+  where
+    (qs, rs) = quotientAndRemainder 0 (== 1) (-) (*) recip xs ys
+{-# INLINE quotRemFractional #-}
+
 quotientAndRemainder
-  :: (Field a, G.Vector v a)
-  => v a
-  -> v a
+  :: (Eq a, G.Vector v a)
+  => a             -- ^ zero
+  -> (a -> Bool)   -- ^ is one?
+  -> (a -> a -> a) -- ^ subtract
+  -> (a -> a -> a) -- ^ multiply
+  -> (a -> a)      -- ^ invert
+  -> v a           -- ^ dividend
+  -> v a           -- ^ divisor
   -> (v a, v a)
-quotientAndRemainder xs ys
-  | G.null ys = throw DivideByZero
-  | G.length xs < G.length ys = (G.empty, xs)
+quotientAndRemainder zer isOne sub mul inv xs ys
+  | lenXs < lenYs = (G.empty, xs)
+  | lenYs == 0 = throw DivideByZero
+  | lenYs == 1 = let invY = inv (G.unsafeHead ys) in
+                 (G.map (`mul` invY) xs, G.empty)
   | otherwise = runST $ do
-    let lenXs = G.length xs
-        lenYs = G.length ys
-        lenQs = lenXs - lenYs + 1
     qs <- MG.unsafeNew lenQs
     rs <- MG.unsafeNew lenXs
     G.unsafeCopy rs xs
+    let yLast = G.unsafeLast ys
+        invYLast = inv yLast
     forM_ [lenQs - 1, lenQs - 2 .. 0] $ \i -> do
       r <- MG.unsafeRead rs (lenYs - 1 + i)
-      let q = r `quot` G.unsafeLast ys
+      let q = if isOne yLast then r else r `mul` invYLast
       MG.unsafeWrite qs i q
-      forM_ [0 .. lenYs - 1] $ \k -> do
-        MG.unsafeModify rs (\c -> c `minus` q `times` G.unsafeIndex ys k) (i + k)
+      MG.unsafeWrite rs (lenYs - 1 + i) zer
+      forM_ [0 .. lenYs - 2] $ \k -> do
+        let y = G.unsafeIndex ys k
+        when (y /= zer) $
+          MG.unsafeModify rs (\c -> c `sub` (q `mul` y)) (i + k)
     let rs' = MG.unsafeSlice 0 lenYs rs
     (,) <$> G.unsafeFreeze qs <*> G.unsafeFreeze rs'
+  where
+    lenXs = G.length xs
+    lenYs = G.length ys
+    lenQs = lenXs - lenYs + 1
 {-# INLINABLE quotientAndRemainder #-}
 
 remainder
-  :: (Field a, G.Vector v a)
+  :: (Eq a, Field a, G.Vector v a)
   => v a
   -> v a
   -> v a
@@ -96,96 +111,27 @@
 {-# INLINABLE remainder #-}
 
 remainderM
-  :: (PrimMonad m, Field a, G.Vector v a)
-  => G.Mutable v (PrimState m) a
-  -> G.Mutable v (PrimState m) a
-  -> m ()
+  :: (Eq a, Field a, G.Vector v a)
+  => G.Mutable v s a
+  -> G.Mutable v s a
+  -> ST s ()
 remainderM xs ys
-  | MG.null ys = throw DivideByZero
-  | MG.length xs < MG.length ys = pure ()
+  | lenXs < lenYs = pure ()
+  | lenYs == 0 = throw DivideByZero
+  | lenYs == 1 = MG.set xs zero
   | otherwise = do
-    let lenXs = MG.length xs
-        lenYs = MG.length ys
-        lenQs = lenXs - lenYs + 1
     yLast <- MG.unsafeRead ys (lenYs - 1)
+    let invYLast = one `quot` yLast
     forM_ [lenQs - 1, lenQs - 2 .. 0] $ \i -> do
       r <- MG.unsafeRead xs (lenYs - 1 + i)
-      forM_ [0 .. lenYs - 1] $ \k -> do
+      MG.unsafeWrite xs (lenYs - 1 + i) zero
+      let q = if yLast == one then r else r `times` invYLast
+      forM_ [0 .. lenYs - 2] $ \k -> do
         y <- MG.unsafeRead ys k
-        -- do not move r / yLast outside the loop,
-        -- because of numerical instability
-        MG.unsafeModify xs (\c -> c `minus` r `times` y `quot` yLast) (i + k)
-{-# INLINABLE remainderM #-}
-
-fieldGcd
-  :: (Eq a, Field a, G.Vector v a)
-  => Poly v a
-  -> Poly v a
-  -> Poly v a
-fieldGcd (Poly xs) (Poly ys) = toPoly' $ runST $ do
-  xs' <- G.thaw xs
-  ys' <- G.thaw ys
-  gcdM xs' ys'
-{-# INLINE fieldGcd #-}
-
-gcdM
-  :: (PrimMonad m, Eq a, Field a, G.Vector v a)
-  => G.Mutable v (PrimState m) a
-  -> G.Mutable v (PrimState m) a
-  -> m (v a)
-gcdM xs ys = do
-  ys' <- dropWhileEndM (== zero) ys
-  if MG.null ys' then G.unsafeFreeze xs else do
-    remainderM xs ys'
-    gcdM ys' xs
-{-# INLINE gcdM #-}
-
--- | Execute the extended Euclidean algorithm.
--- For polynomials @a@ and @b@, compute their unique greatest common divisor @g@
--- and the unique coefficient polynomial @s@ satisfying @as + bt = g@,
--- such that either @g@ is monic, or @g = 0@ and @s@ is monic, or @g = s = 0@.
---
--- >>> gcdExt (X^2 + 1 :: UPoly Double) (X^3 + 3 * X :: UPoly Double)
--- (1.0, 0.5 * X^2 + (-0.0) * X + 1.0)
--- >>> gcdExt (X^3 + 3 * X :: UPoly Double) (3 * X^4 + 3 * X^2 :: UPoly Double)
--- (1.0 * X + 0.0,(-0.16666666666666666) * X^2 + (-0.0) * X + 0.3333333333333333)
-gcdExt
-  :: (Eq a, Field a, G.Vector v a, Eq (v a))
-  => Poly v a
-  -> Poly v a
-  -> (Poly v a, Poly v a)
-gcdExt xs ys = case scaleMonic gs of
-  Just (c', gs') -> (gs', scale' zero c' ss)
-  Nothing -> case scaleMonic ss of
-    Just (_, ss') -> (zero, ss')
-    Nothing -> (zero, zero)
+        when (y /= zero) $
+          MG.unsafeModify xs (\c -> c `minus` q `times` y) (i + k)
   where
-    (gs, ss) = go ys xs zero one
-      where
-        go r' r s' s
-          | r' == zero = (r, s)
-          | otherwise  = case r `quotRem` r' of
-            (q, r'') -> go r'' r' (s `minus` q `times` s') s'
-{-# INLINABLE gcdExt #-}
-
--- | Scale a non-zero polynomial such that its leading coefficient is one,
--- returning the reciprocal of the leading coefficient in the scaling.
---
--- >>> scaleMonic (X^3 + 3 * X :: UPoly Double)
--- Just (1.0, 1.0 * X^3 + 0.0 * X^2 + 3.0 * X + 0.0)
--- >>> scaleMonic (3 * X^4 + 3 * X^2 :: UPoly Double)
--- Just (0.3333333333333333, 1.0 * X^4 + 0.0 * X^3 + 1.0 * X^2 + 0.0 * X + 0.0)
-scaleMonic
-  :: (Eq a, Field a, G.Vector v a, Eq (v a))
-  => Poly v a
-  -> Maybe (a, Poly v a)
-scaleMonic xs = case leading xs of
-  Nothing -> Nothing
-  Just (_, c) -> let c' = one `quot` c in Just (c', scale' zero c' xs)
-{-# INLINE scaleMonic #-}
-
-#else
-
-module Data.Poly.Internal.Dense.Field () where
-
-#endif
+    lenXs = MG.length xs
+    lenYs = MG.length ys
+    lenQs = lenXs - lenYs + 1
+{-# INLINABLE remainderM #-}
diff --git a/src/Data/Poly/Internal/Dense/GcdDomain.hs b/src/Data/Poly/Internal/Dense/GcdDomain.hs
--- a/src/Data/Poly/Internal/Dense/GcdDomain.hs
+++ b/src/Data/Poly/Internal/Dense/GcdDomain.hs
@@ -4,12 +4,11 @@
 -- Licence:     BSD3
 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
 --
--- GcdDomain for GcdDomain underlying
+-- 'GcdDomain' instance with a 'GcdDomain' constraint on the coefficient type.
 --
 
-{-# LANGUAGE CPP                        #-}
 {-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE PatternSynonyms            #-}
+{-# LANGUAGE MultiWayIf                 #-}
 {-# LANGUAGE ScopedTypeVariables        #-}
 {-# LANGUAGE TypeFamilies               #-}
 
@@ -18,33 +17,40 @@
 module Data.Poly.Internal.Dense.GcdDomain
   () where
 
-#if MIN_VERSION_semirings(0,4,2)
-
 import Prelude hiding (gcd, lcm, (^))
 import Control.Exception
 import Control.Monad
-import Control.Monad.Primitive
 import Control.Monad.ST
 import Data.Euclidean
+import Data.Maybe
 import Data.Semiring (Semiring(..), Ring(), isZero, minus)
 import qualified Data.Vector.Generic as G
 import qualified Data.Vector.Generic.Mutable as MG
 
 import Data.Poly.Internal.Dense
 
--- | Consider using 'Data.Poly.Semiring.PolyOverField' wrapper,
--- which provides a much faster implementation of
--- 'Data.Euclidean.gcd' for polynomials over 'Field'.
-instance (Eq a, Ring a, GcdDomain a, Eq (v a), G.Vector v a) => GcdDomain (Poly v a) where
+-- | @since 0.3.0.0
+instance (Eq a, Ring a, GcdDomain a, G.Vector v a) => GcdDomain (Poly v a) where
   divide (Poly xs) (Poly ys) =
     toPoly' <$> quotient xs ys
+  {-# INLINABLE divide #-}
 
   gcd (Poly xs) (Poly ys)
     | G.null xs = Poly ys
     | G.null ys = Poly xs
+    | G.length xs == 1 = Poly $ G.singleton $ G.foldl' gcd (G.unsafeHead xs) ys
+    | G.length ys == 1 = Poly $ G.singleton $ G.foldl' gcd (G.unsafeHead ys) xs
     | otherwise = toPoly' $ gcdNonEmpty xs ys
   {-# INLINE gcd #-}
 
+  lcm x@(Poly xs) y@(Poly ys)
+    | G.null xs || G.null ys = zero
+    | otherwise = (x `divide'` gcd x y) `times` y
+  {-# INLINABLE lcm #-}
+
+  coprime x y = isJust (one `divide` gcd x y)
+  {-# INLINABLE coprime #-}
+
 gcdNonEmpty
   :: (Eq a, Ring a, GcdDomain a, G.Vector v a)
   => v a
@@ -66,20 +72,17 @@
     a <- MG.unsafeRead zs' (lenZs - 1)
     z <- go a (lenZs - 1)
 
-    let err = error "gcdNonEmpty: violated internal invariant"
     forM_ [0 .. lenZs - 1] $ \i ->
-      MG.unsafeModify
-        zs'
-        (\c -> maybe err (`times` xy) (c `divide` z))
-        i
+      MG.unsafeModify zs'((`times` xy) . (`divide'` z)) i
 
     G.unsafeFreeze zs'
+{-# INLINABLE gcdNonEmpty #-}
 
 gcdM
-  :: (PrimMonad m, Eq a, Ring a, GcdDomain a, G.Vector v a)
-  => G.Mutable v (PrimState m) a
-  -> G.Mutable v (PrimState m) a
-  -> m (G.Mutable v (PrimState m) a)
+  :: (Eq a, Ring a, GcdDomain a, G.Vector v a)
+  => G.Mutable v s a
+  -> G.Mutable v s a
+  -> ST s (G.Mutable v s a)
 gcdM xs ys
   | MG.null xs = pure ys
   | MG.null ys = pure xs
@@ -88,42 +91,64 @@
       lenYs = MG.length ys
   xLast <- MG.unsafeRead xs (lenXs - 1)
   yLast <- MG.unsafeRead ys (lenYs - 1)
-  let z = xLast `lcm` yLast
-      zx = case z `divide` xLast of
-        Nothing -> error "gcdM: highest coefficient is 0"
-        Just t  -> t
-      zy = case z `divide` yLast of
-        Nothing -> error "gcdM: highest coefficient is 0"
-        Just t  -> t
+  let z  = xLast `lcm` yLast
+      zx = z `divide'` xLast
+      zy = z `divide'` yLast
 
-  if lenXs <= lenYs then do
-    forM_ [0 .. lenXs - 1] $ \i -> do
-      x <- MG.unsafeRead xs i
-      MG.unsafeModify
-        ys
-        (\y -> (y `times` zy) `minus` x `times` zx)
-        (i + lenYs - lenXs)
-    forM_ [0 .. lenYs - lenXs - 1] $
-      MG.unsafeModify ys (`times` zy)
-    ys' <- dropWhileEndM isZero ys
-    gcdM xs ys'
-  else do
-    forM_ [0 .. lenYs - 1] $ \i -> do
-      y <- MG.unsafeRead ys i
-      MG.unsafeModify
-        xs
-        (\x -> (x `times` zx) `minus` y `times` zy)
-        (i + lenXs - lenYs)
-    forM_ [0 .. lenXs - lenYs - 1] $
-      MG.unsafeModify xs (`times` zx)
-    xs' <- dropWhileEndM isZero xs
-    gcdM xs' ys
+  if
+    | lenYs <= lenXs
+    , Just xy <- xLast `divide` yLast -> do
+      forM_ [0 .. lenYs - 1] $ \i -> do
+        y <- MG.unsafeRead ys i
+        when (y /= zero) $
+          MG.unsafeModify
+            xs
+            (\x -> x `minus` y `times` xy)
+            (i + lenXs - lenYs)
+      xs' <- dropWhileEndM isZero xs
+      gcdM xs' ys
+    | lenXs <= lenYs
+    , Just yx <- yLast `divide` xLast -> do
+      forM_ [0 .. lenXs - 1] $ \i -> do
+        x <- MG.unsafeRead xs i
+        when (x /= zero) $
+          MG.unsafeModify
+            ys
+            (\y -> y `minus` x `times` yx)
+            (i + lenYs - lenXs)
+      ys' <- dropWhileEndM isZero ys
+      gcdM xs ys'
+    | lenYs <= lenXs -> do
+      forM_ [0 .. lenYs - 1] $ \i -> do
+        y <- MG.unsafeRead ys i
+        MG.unsafeModify
+          xs
+          (\x -> x `times` zx `minus` y `times` zy)
+          (i + lenXs - lenYs)
+      forM_ [0 .. lenXs - lenYs - 1] $
+        MG.unsafeModify xs (`times` zx)
+      xs' <- dropWhileEndM isZero xs
+      gcdM xs' ys
+    | otherwise -> do
+      forM_ [0 .. lenXs - 1] $ \i -> do
+        x <- MG.unsafeRead xs i
+        MG.unsafeModify
+          ys
+          (\y -> y `times` zy `minus` x `times` zx)
+          (i + lenYs - lenXs)
+      forM_ [0 .. lenYs - lenXs - 1] $
+        MG.unsafeModify ys (`times` zy)
+      ys' <- dropWhileEndM isZero ys
+      gcdM xs ys'
 {-# INLINABLE gcdM #-}
 
+divide' :: GcdDomain a => a -> a -> a
+divide' = (fromMaybe (error "gcd: violated internal invariant") .) . divide
+
 isZeroM
-  :: (Eq a, Semiring a, PrimMonad m, G.Vector v a)
-  => G.Mutable v (PrimState m) a
-  -> m Bool
+  :: (Eq a, Semiring a, G.Vector v a)
+  => G.Mutable v s a
+  -> ST s Bool
 isZeroM xs = go (MG.length xs)
   where
     go 0 = pure True
@@ -133,7 +158,7 @@
 {-# INLINE isZeroM #-}
 
 quotient
-  :: (Eq a, Eq (v a), Ring a, GcdDomain a, G.Vector v a)
+  :: (Eq a, Ring a, GcdDomain a, G.Vector v a)
   => v a
   -> v a
   -> Maybe (v a)
@@ -161,7 +186,7 @@
               Nothing -> pure Nothing
               Just q -> do
                 MG.unsafeWrite qs i q
-                forM_ [0 .. lenYs - 1] $ \k -> do
+                forM_ [0 .. lenYs - 1] $ \k ->
                   MG.unsafeModify
                     rs
                     (\c -> c `minus` q `times` G.unsafeIndex ys k)
@@ -170,5 +195,3 @@
 
     go (lenQs - 1)
 {-# INLINABLE quotient #-}
-
-#endif
diff --git a/src/Data/Poly/Internal/Dense/Laurent.hs b/src/Data/Poly/Internal/Dense/Laurent.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Dense/Laurent.hs
@@ -0,0 +1,326 @@
+-- |
+-- Module:      Data.Poly.Internal.Dense.Laurent
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>.
+--
+
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE KindSignatures             #-}
+{-# LANGUAGE PatternSynonyms            #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE ViewPatterns               #-}
+
+module Data.Poly.Internal.Dense.Laurent
+  ( Laurent
+  , VLaurent
+  , ULaurent
+  , unLaurent
+  , toLaurent
+  , leading
+  , monomial
+  , scale
+  , pattern X
+  , (^-)
+  , eval
+  , subst
+  , deriv
+  ) where
+
+import Prelude hiding (quotRem, quot, rem, gcd, lcm)
+import Control.Arrow (first)
+import Control.DeepSeq (NFData(..))
+import Control.Exception
+import Data.Euclidean (GcdDomain(..), Euclidean(..), Field)
+import Data.Kind
+import Data.List (intersperse)
+import Data.Semiring (Semiring(..), Ring())
+import qualified Data.Semiring as Semiring
+import qualified Data.Vector as V
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed as U
+
+import Data.Poly.Internal.Dense (Poly(..))
+import qualified Data.Poly.Internal.Dense as Dense
+import Data.Poly.Internal.Dense.Field ()
+import Data.Poly.Internal.Dense.GcdDomain ()
+
+-- | <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>
+-- of one variable with coefficients from @a@,
+-- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
+--
+-- Use the pattern 'X' and the '^-' operator for construction:
+--
+-- >>> (X + 1) + (X^-1 - 1) :: VLaurent Integer
+-- 1 * X + 0 + 1 * X^-1
+-- >>> (X + 1) * (1 - X^-1) :: ULaurent Int
+-- 1 * X + 0 + (-1) * X^-1
+--
+-- Polynomials are stored normalized, without leading
+-- and trailing
+-- zero coefficients, so 0 * X + 1 + 0 * X^-1 equals to 1.
+--
+-- The 'Ord' instance does not make much sense mathematically,
+-- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
+--
+-- Due to being polymorphic by multiple axis, the performance of `Laurent` crucially
+-- depends on specialisation of instances. Clients are strongly recommended
+-- to compile with @ghc-options:@ @-fspecialise-aggressively@ and suggested to enable @-O2@.
+--
+-- @since 0.4.0.0
+--
+data Laurent (v :: Type -> Type) (a :: Type) = Laurent !Int !(Poly v a)
+  deriving (Eq, Ord)
+
+-- | Deconstruct a 'Laurent' polynomial into an offset (largest possible)
+-- and a regular polynomial.
+--
+-- >>> unLaurent (2 * X + 1 :: ULaurent Int)
+-- (0,2 * X + 1)
+-- >>> unLaurent (1 + 2 * X^-1 :: ULaurent Int)
+-- (-1,1 * X + 2)
+-- >>> unLaurent (2 * X^2 + X :: ULaurent Int)
+-- (1,2 * X + 1)
+-- >>> unLaurent (0 :: ULaurent Int)
+-- (0,0)
+--
+-- @since 0.4.0.0
+unLaurent :: Laurent v a -> (Int, Poly v a)
+unLaurent (Laurent off poly) = (off, poly)
+
+-- | Construct 'Laurent' polynomial from an offset and a regular polynomial.
+-- One can imagine it as 'Data.Poly.Semiring.scale', but allowing negative offsets.
+--
+-- >>> toLaurent 2 (2 * Data.Poly.X + 1) :: ULaurent Int
+-- 2 * X^3 + 1 * X^2
+-- >>> toLaurent (-2) (2 * Data.Poly.X + 1) :: ULaurent Int
+-- 2 * X^-1 + 1 * X^-2
+--
+-- @since 0.4.0.0
+toLaurent
+  :: (Eq a, Semiring a, G.Vector v a)
+  => Int
+  -> Poly v a
+  -> Laurent v a
+toLaurent off (Poly xs) = go 0
+  where
+    go k
+      | k >= G.length xs
+      = Laurent 0 zero
+      | G.unsafeIndex xs k == zero
+      = go (k + 1)
+      | otherwise
+      = Laurent (off + k) (Poly (G.unsafeDrop k xs))
+{-# INLINE toLaurent #-}
+
+toLaurentNum
+  :: (Eq a, Num a, G.Vector v a)
+  => Int
+  -> Poly v a
+  -> Laurent v a
+toLaurentNum off (Poly xs) = go 0
+  where
+    go k
+      | k >= G.length xs
+      = Laurent 0 0
+      | G.unsafeIndex xs k == 0
+      = go (k + 1)
+      | otherwise
+      = Laurent (off + k) (Poly (G.unsafeDrop k xs))
+{-# INLINE toLaurentNum #-}
+
+instance NFData (v a) => NFData (Laurent v a) where
+  rnf (Laurent off poly) = rnf off `seq` rnf poly
+
+instance (Show a, G.Vector v a) => Show (Laurent v a) where
+  showsPrec d (Laurent off poly)
+    | G.null (unPoly poly)
+      = showString "0"
+    | otherwise
+      = showParen (d > 0)
+      $ foldl (.) id
+      $ intersperse (showString " + ")
+      $ G.ifoldl (\acc i c -> showCoeff (i + off) c : acc) []
+      $ unPoly poly
+    where
+      -- Negative powers should be displayed without surrounding brackets
+      showCoeff 0 c = showsPrec 7 c
+      showCoeff 1 c = showsPrec 7 c . showString " * X"
+      showCoeff i c = showsPrec 7 c . showString (" * X^" ++ show i)
+
+-- | Laurent polynomials backed by boxed vectors.
+--
+-- @since 0.4.0.0
+type VLaurent = Laurent V.Vector
+
+-- | Laurent polynomials backed by unboxed vectors.
+--
+-- @since 0.4.0.0
+type ULaurent = Laurent U.Vector
+
+-- | Return the leading power and coefficient of a non-zero polynomial.
+--
+-- >>> leading ((2 * X + 1) * (2 * X^2 - 1) :: ULaurent Int)
+-- Just (3,4)
+-- >>> leading (0 :: ULaurent Int)
+-- Nothing
+--
+-- @since 0.4.0.0
+leading :: G.Vector v a => Laurent v a -> Maybe (Int, a)
+leading (Laurent off poly) = first ((+ off) . fromIntegral) <$> Dense.leading poly
+{-# INLINABLE leading #-}
+
+-- | Note that 'abs' = 'id' and 'signum' = 'const' 1.
+instance (Eq a, Num a, G.Vector v a) => Num (Laurent v a) where
+  Laurent off1 poly1 * Laurent off2 poly2 = toLaurentNum (off1 + off2) (poly1 * poly2)
+  Laurent off1 poly1 + Laurent off2 poly2 = case off1 `compare` off2 of
+    LT -> toLaurentNum off1 (poly1 + Dense.scale (fromIntegral $ off2 - off1) 1 poly2)
+    EQ -> toLaurentNum off1 (poly1 + poly2)
+    GT -> toLaurentNum off2 (Dense.scale (fromIntegral $ off1 - off2) 1 poly1 + poly2)
+  Laurent off1 poly1 - Laurent off2 poly2 = case off1 `compare` off2 of
+    LT -> toLaurentNum off1 (poly1 - Dense.scale (fromIntegral $ off2 - off1) 1 poly2)
+    EQ -> toLaurentNum off1 (poly1 - poly2)
+    GT -> toLaurentNum off2 (Dense.scale (fromIntegral $ off1 - off2) 1 poly1 - poly2)
+  negate (Laurent off poly) = Laurent off (negate poly)
+  abs = id
+  signum = const 1
+  fromInteger n = Laurent 0 (fromInteger n)
+  {-# INLINE (+) #-}
+  {-# INLINE (-) #-}
+  {-# INLINE negate #-}
+  {-# INLINE fromInteger #-}
+  {-# INLINE (*) #-}
+
+instance (Eq a, Semiring a, G.Vector v a) => Semiring (Laurent v a) where
+  zero = Laurent 0 zero
+  one  = Laurent 0 one
+  Laurent off1 poly1 `times` Laurent off2 poly2 =
+    toLaurent (off1 + off2) (poly1 `times` poly2)
+  Laurent off1 poly1 `plus` Laurent off2 poly2 = case off1 `compare` off2 of
+    LT -> toLaurent off1 (poly1 `plus` Dense.scale' (fromIntegral $ off2 - off1) one poly2)
+    EQ -> toLaurent off1 (poly1 `plus` poly2)
+    GT -> toLaurent off2 (Dense.scale' (fromIntegral $ off1 - off2) one poly1 `plus` poly2)
+  fromNatural n = Laurent 0 (fromNatural n)
+  {-# INLINE zero #-}
+  {-# INLINE one #-}
+  {-# INLINE plus #-}
+  {-# INLINE times #-}
+  {-# INLINE fromNatural #-}
+
+instance (Eq a, Ring a, G.Vector v a) => Ring (Laurent v a) where
+  negate (Laurent off poly) = Laurent off (Semiring.negate poly)
+
+-- | Create a monomial from a power and a coefficient.
+--
+-- @since 0.4.0.0
+monomial :: (Eq a, Semiring a, G.Vector v a) => Int -> a -> Laurent v a
+monomial p c
+  | c == zero = Laurent 0 zero
+  | otherwise = Laurent p (Dense.monomial' 0 c)
+{-# INLINE monomial #-}
+
+-- | Multiply a polynomial by a monomial, expressed as a power and a coefficient.
+--
+-- >>> scale 2 3 (X^-2 + 1) :: ULaurent Int
+-- 3 * X^2 + 0 * X + 3
+--
+-- @since 0.4.0.0
+scale :: (Eq a, Semiring a, G.Vector v a) => Int -> a -> Laurent v a -> Laurent v a
+scale yp yc (Laurent off poly) = toLaurent (off + yp) (Dense.scale' 0 yc poly)
+{-# INLINABLE scale #-}
+
+-- | Evaluate the polynomial at a given point.
+--
+-- >>> eval (X^-2 + 1 :: ULaurent Double) 2
+-- 1.25
+--
+-- @since 0.4.0.0
+eval :: (Field a, G.Vector v a) => Laurent v a -> a -> a
+eval (Laurent off poly) x = Dense.eval' poly x `times`
+  (if off >= 0 then x Semiring.^ off else quot one x Semiring.^ (- off))
+{-# INLINE eval #-}
+
+-- | Substitute another polynomial instead of 'Data.Poly.X'.
+--
+-- >>> import Data.Poly (UPoly)
+-- >>> subst (Data.Poly.X^2 + 1 :: UPoly Int) (X^-1 + 1 :: ULaurent Int)
+-- 2 + 2 * X^-1 + 1 * X^-2
+--
+-- @since 0.4.0.0
+subst :: (Eq a, Semiring a, G.Vector v a, G.Vector w a) => Poly v a -> Laurent w a -> Laurent w a
+subst = Dense.substitute' (scale 0)
+{-# INLINE subst #-}
+
+-- | Take the derivative of the polynomial.
+--
+-- >>> deriv (X^-1 + 3 * X) :: ULaurent Int
+-- 3 + 0 * X^-1 + (-1) * X^-2
+--
+-- @since 0.4.0.0
+deriv :: (Eq a, Ring a, G.Vector v a) => Laurent v a -> Laurent v a
+deriv (Laurent off (Poly xs)) =
+  toLaurent (off - 1) $ Dense.toPoly' $ G.imap (times . Semiring.fromIntegral . (+ off)) xs
+{-# INLINE deriv #-}
+
+-- | The polynomial 'X'.
+--
+-- > X == monomial 1 one
+--
+-- @since 0.4.0.0
+pattern X :: (Eq a, Semiring a, G.Vector v a) => Laurent v a
+pattern X <- (isVar -> True)
+  where X = var
+
+var :: forall a v. (Eq a, Semiring a, G.Vector v a) => Laurent v a
+var
+  | (one :: a) == zero = Laurent 0 zero
+  | otherwise          = Laurent 1 one
+{-# INLINE var #-}
+
+isVar :: forall v a. (Eq a, Semiring a, G.Vector v a) => Laurent v a -> Bool
+isVar (Laurent off (Poly xs))
+  | (one :: a) == zero = off == 0 && G.null xs
+  | otherwise          = off == 1 && G.length xs == 1 && G.unsafeHead xs == one
+{-# INLINE isVar #-}
+
+-- | Used to construct monomials with negative powers.
+--
+-- This operator can be applied only to monomials with unit coefficients,
+-- but is instrumental to express Laurent polynomials
+-- in a mathematical fashion:
+--
+-- >>> X^-3 :: ULaurent Int
+-- 1 * X^-3
+-- >>> X + 2 + 3 * (X^2)^-1 :: ULaurent Int
+-- 1 * X + 2 + 0 * X^-1 + 3 * X^-2
+--
+-- @since 0.4.0.0
+(^-)
+  :: (Eq a, Num a, G.Vector v a)
+  => Laurent v a
+  -> Int
+  -> Laurent v a
+Laurent off (Poly xs) ^- n
+  | G.length xs == 1, G.unsafeHead xs == 1
+  = Laurent (off * (-n)) (Poly xs)
+  | otherwise
+  = throw $ PatternMatchFail "(^-) can be applied only to a monom with unit coefficient"
+
+instance (Eq a, Ring a, GcdDomain a, G.Vector v a) => GcdDomain (Laurent v a) where
+  divide (Laurent off1 poly1) (Laurent off2 poly2) =
+    toLaurent (off1 - off2) <$> divide poly1 poly2
+  {-# INLINE divide #-}
+
+  gcd (Laurent _ poly1) (Laurent _ poly2) =
+    toLaurent 0 (gcd poly1 poly2)
+  {-# INLINE gcd #-}
+
+  lcm (Laurent _ poly1) (Laurent _ poly2) =
+    toLaurent 0 (lcm poly1 poly2)
+  {-# INLINE lcm #-}
+
+  coprime (Laurent _ poly1) (Laurent _ poly2) =
+    coprime poly1 poly2
+  {-# INLINE coprime #-}
diff --git a/src/Data/Poly/Internal/Multi.hs b/src/Data/Poly/Internal/Multi.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Multi.hs
@@ -0,0 +1,612 @@
+-- |
+-- Module:      Data.Poly.Internal.Multi
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE PatternSynonyms            #-}
+{-# LANGUAGE PolyKinds                  #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE StandaloneDeriving         #-}
+{-# LANGUAGE TypeApplications           #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE TypeOperators              #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE ViewPatterns               #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Data.Poly.Internal.Multi
+  ( MultiPoly(..)
+  , VMultiPoly
+  , UMultiPoly
+  , toMultiPoly
+  , toMultiPoly'
+  , leading
+  , monomial
+  , monomial'
+  , scale
+  , scale'
+  , pattern X
+  , pattern Y
+  , pattern Z
+  , pattern X'
+  , pattern Y'
+  , pattern Z'
+  , eval
+  , eval'
+  , subst
+  , subst'
+  , substitute
+  , substitute'
+  , deriv
+  , deriv'
+  , integral
+  , integral'
+  -- * Univariate polynomials
+  , Poly
+  , VPoly
+  , UPoly
+  , unPoly
+  -- * Conversions
+  , segregate
+  , unsegregate
+  ) where
+
+import Prelude hiding (quot, gcd)
+import Control.Arrow
+import Control.DeepSeq
+import Data.Coerce
+import Data.Euclidean (Field, quot)
+import Data.Finite
+import Data.Kind
+import Data.List (intersperse)
+import Data.Semiring (Semiring(..), Ring())
+import qualified Data.Semiring as Semiring
+import qualified Data.Vector as V
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Sized as SG
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Sized as SU
+import qualified Data.Vector.Sized as SV
+import GHC.Exts (IsList(..))
+import GHC.TypeNats (KnownNat, Nat, type (+), type (<=))
+
+import Data.Poly.Internal.Multi.Core
+
+-- | Sparse polynomials of @n@ variables with coefficients from @a@,
+-- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
+--
+-- Use the patterns 'Data.Poly.Multi.X',
+-- 'Data.Poly.Multi.Y' and
+-- 'Data.Poly.Multi.Z' for construction:
+--
+-- >>> :set -XDataKinds
+-- >>> (X + 1) + (Y - 1) + Z :: VMultiPoly 3 Integer
+-- 1 * X + 1 * Y + 1 * Z
+-- >>> (X + 1) * (Y - 1) :: UMultiPoly 2 Int
+-- 1 * X * Y + (-1) * X + 1 * Y + (-1)
+--
+-- Polynomials are stored normalized, without
+-- zero coefficients, so 0 * 'Data.Poly.Multi.X' + 1 equals to 1.
+--
+-- The 'Ord' instance does not make much sense mathematically,
+-- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
+--
+-- Due to being polymorphic by multiple axis, the performance of `MultiPoly` crucially
+-- depends on specialisation of instances. Clients are strongly recommended
+-- to compile with @ghc-options:@ @-fspecialise-aggressively@ and suggested to enable @-O2@.
+--
+-- @since 0.5.0.0
+newtype MultiPoly (v :: Type -> Type) (n :: Nat) (a :: Type) = MultiPoly
+  { unMultiPoly :: v (SU.Vector n Word, a)
+  -- ^ Convert a 'MultiPoly' to a vector of (powers, coefficient) pairs.
+  --
+  -- @since 0.5.0.0
+  }
+
+deriving instance Eq     (v (SU.Vector n Word, a)) => Eq     (MultiPoly v n a)
+deriving instance Ord    (v (SU.Vector n Word, a)) => Ord    (MultiPoly v n a)
+deriving instance NFData (v (SU.Vector n Word, a)) => NFData (MultiPoly v n a)
+
+-- | Multivariate polynomials backed by boxed vectors.
+--
+-- @since 0.5.0.0
+type VMultiPoly (n :: Nat) (a :: Type) = MultiPoly V.Vector n a
+
+-- | Multivariate polynomials backed by unboxed vectors.
+--
+-- @since 0.5.0.0
+type UMultiPoly (n :: Nat) (a :: Type) = MultiPoly U.Vector n a
+
+-- | Sparse univariate polynomials with coefficients from @a@,
+-- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
+--
+-- Use pattern 'Data.Poly.Multi.X' for construction:
+--
+-- >>> (X + 1) + (X - 1) :: VPoly Integer
+-- 2 * X
+-- >>> (X + 1) * (X - 1) :: UPoly Int
+-- 1 * X^2 + (-1)
+--
+-- Polynomials are stored normalized, without
+-- zero coefficients, so 0 * 'Data.Poly.Multi.X' + 1 equals to 1.
+--
+-- 'Ord' instance does not make much sense mathematically,
+-- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
+--
+-- Due to being polymorphic by multiple axis, the performance of `Poly` crucially
+-- depends on specialisation of instances. Clients are strongly recommended
+-- to compile with @ghc-options:@ @-fspecialise-aggressively@ and suggested to enable @-O2@.
+--
+-- @since 0.3.0.0
+type Poly (v :: Type -> Type) (a :: Type) = MultiPoly v 1 a
+
+-- | Polynomials backed by boxed vectors.
+--
+-- @since 0.3.0.0
+type VPoly (a :: Type) = Poly V.Vector a
+
+-- | Polynomials backed by unboxed vectors.
+--
+-- @since 0.3.0.0
+type UPoly (a :: Type) = Poly U.Vector a
+
+-- | Convert a 'Poly' to a vector of coefficients.
+--
+-- @since 0.3.0.0
+unPoly
+  :: (G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> v (Word, a)
+unPoly = G.map (first SU.head) . unMultiPoly
+
+instance (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a)) => IsList (MultiPoly v n a) where
+  type Item (MultiPoly v n a) = (SU.Vector n Word, a)
+  fromList = toMultiPoly' . G.fromList
+  fromListN = (toMultiPoly' .) . G.fromListN
+  toList = G.toList . unMultiPoly
+
+instance (Show a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Show (MultiPoly v n a) where
+  showsPrec d (MultiPoly xs)
+    | G.null xs
+      = showString "0"
+    | otherwise
+      = showParen (d > 0)
+      $ foldl (.) id
+      $ intersperse (showString " + ")
+      $ G.foldl (\acc (is, c) -> showCoeff is c : acc) [] xs
+    where
+      showCoeff is c
+        = showsPrec 7 c . foldl (.) id
+        ( map ((showString " * " .) . uncurry showPower)
+        $ filter ((/= 0) . fst)
+        $ zip (SU.toList is) (finites :: [Finite n]))
+
+      -- Powers are guaranteed to be non-negative
+      showPower :: Word -> Finite n -> String -> String
+      showPower 1 n = showString (showVar n)
+      showPower i n = showString (showVar n) . showString ("^" ++ show i)
+
+      showVar :: Finite n -> String
+      showVar = \case
+        0 -> "X"
+        1 -> "Y"
+        2 -> "Z"
+        k -> "X" ++ show k
+
+-- | Make a 'MultiPoly' from a list of (powers, coefficient) pairs.
+--
+-- >>> :set -XOverloadedLists -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> toMultiPoly [(fromTuple (0,0),1),(fromTuple (0,1),2),(fromTuple (1,0),3)] :: VMultiPoly 2 Integer
+-- 3 * X + 2 * Y + 1
+-- >>> toMultiPoly [(fromTuple (0,0),0),(fromTuple (0,1),0),(fromTuple (1,0),0)] :: UMultiPoly 2 Int
+-- 0
+--
+-- @since 0.5.0.0
+toMultiPoly
+  :: (Eq a, Num a, G.Vector v (SU.Vector n Word, a))
+  => v (SU.Vector n Word, a)
+  -> MultiPoly v n a
+toMultiPoly = MultiPoly . normalize (/= 0) (+)
+{-# INLINABLE toMultiPoly #-}
+
+toMultiPoly'
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a))
+  => v (SU.Vector n Word, a)
+  -> MultiPoly v n a
+toMultiPoly' = MultiPoly . normalize (/= zero) plus
+{-# INLINABLE toMultiPoly' #-}
+
+-- | Note that 'abs' = 'id' and 'signum' = 'const' 1.
+instance (Eq a, Num a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Num (MultiPoly v n a) where
+
+  (+) = coerce (plusPoly    @v @(SU.Vector n Word) @a (/= 0) (+))
+  (-) = coerce (minusPoly   @v @(SU.Vector n Word) @a (/= 0) negate (-))
+  (*) = coerce (convolution @v @(SU.Vector n Word) @a (/= 0) (+) (*))
+
+  negate (MultiPoly xs) = MultiPoly $ G.map (fmap negate) xs
+  abs = id
+  signum = const 1
+  fromInteger n = case fromInteger n of
+    0 -> MultiPoly G.empty
+    m -> MultiPoly $ G.singleton (0, m)
+
+  {-# INLINE (+) #-}
+  {-# INLINE (-) #-}
+  {-# INLINE negate #-}
+  {-# INLINE fromInteger #-}
+  {-# INLINE (*) #-}
+
+instance (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Semiring (MultiPoly v n a) where
+  zero = MultiPoly G.empty
+  one
+    | (one :: a) == zero = zero
+    | otherwise = MultiPoly $ G.singleton (0, one)
+
+  plus  = coerce (plusPoly    @v @(SU.Vector n Word) @a (/= zero) plus)
+  times = coerce (convolution @v @(SU.Vector n Word) @a (/= zero) plus times)
+
+  {-# INLINE zero #-}
+  {-# INLINE one #-}
+  {-# INLINE plus #-}
+  {-# INLINE times #-}
+
+  fromNatural n = if n' == zero then zero else MultiPoly $ G.singleton (0, n')
+    where
+      n' :: a
+      n' = fromNatural n
+  {-# INLINE fromNatural #-}
+
+instance (Eq a, Ring a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Ring (MultiPoly v n a) where
+  negate (MultiPoly xs) = MultiPoly $ G.map (fmap Semiring.negate) xs
+
+-- | Return the leading power and coefficient of a non-zero polynomial.
+--
+-- >>> import Data.Poly.Sparse (UPoly)
+-- >>> leading ((2 * X + 1) * (2 * X^2 - 1) :: UPoly Int)
+-- Just (3,4)
+-- >>> leading (0 :: UPoly Int)
+-- Nothing
+--
+-- @since 0.3.0.0
+leading :: G.Vector v (SU.Vector 1 Word, a) => Poly v a -> Maybe (Word, a)
+leading (MultiPoly v)
+  | G.null v  = Nothing
+  | otherwise = Just $ first SU.head $ G.last v
+
+-- | Multiply a polynomial by a monomial, expressed as powers and a coefficient.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> scale (fromTuple (1, 1)) 3 (X^2 + Y) :: UMultiPoly 2 Int
+-- 3 * X^3 * Y + 3 * X * Y^2
+--
+-- @since 0.5.0.0
+scale
+  :: (Eq a, Num a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Word
+  -> a
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+scale yp yc = MultiPoly . scaleInternal (/= 0) (*) yp yc . unMultiPoly
+
+scale'
+  :: (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Word
+  -> a
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+scale' yp yc = MultiPoly . scaleInternal (/= zero) times yp yc . unMultiPoly
+
+-- | Create a monomial from powers and a coefficient.
+--
+-- @since 0.5.0.0
+monomial
+  :: (Eq a, Num a, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Word
+  -> a
+  -> MultiPoly v n a
+monomial p c
+  | c == 0    = MultiPoly G.empty
+  | otherwise = MultiPoly $ G.singleton (p, c)
+{-# INLINABLE monomial #-}
+
+monomial'
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Word
+  -> a
+  -> MultiPoly v n a
+monomial' p c
+  | c == zero = MultiPoly G.empty
+  | otherwise = MultiPoly $ G.singleton (p, c)
+{-# INLINABLE monomial' #-}
+
+-- | Evaluate the polynomial at a given point.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> eval (X^2 + Y^2 :: UMultiPoly 2 Int) (fromTuple (3, 4) :: Data.Vector.Sized.Vector 2 Int)
+-- 25
+--
+-- @since 0.5.0.0
+eval
+  :: (Num a, G.Vector v (SU.Vector n Word, a), G.Vector u a)
+  => MultiPoly v n a
+  -> SG.Vector u n a
+  -> a
+eval = substitute (*)
+{-# INLINE eval #-}
+
+eval'
+  :: (Semiring a, G.Vector v (SU.Vector n Word, a), G.Vector u a)
+  => MultiPoly v n a
+  -> SG.Vector u n a
+  -> a
+eval' = substitute' times
+{-# INLINE eval' #-}
+
+-- | Substitute other polynomials instead of the variables.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> subst (X^2 + Y^2 + Z^2 :: UMultiPoly 3 Int) (fromTuple (X + 1, Y + 1, X + Y :: UMultiPoly 2 Int))
+-- 2 * X^2 + 2 * X * Y + 2 * X + 2 * Y^2 + 2 * Y + 2
+--
+-- @since 0.5.0.0
+subst
+  :: (Eq a, Num a, KnownNat m, G.Vector v (SU.Vector n Word, a), G.Vector w (SU.Vector m Word, a))
+  => MultiPoly v n a
+  -> SV.Vector n (MultiPoly w m a)
+  -> MultiPoly w m a
+subst = substitute (scale 0)
+{-# INLINE subst #-}
+
+subst'
+  :: (Eq a, Semiring a, KnownNat m, G.Vector v (SU.Vector n Word, a), G.Vector w (SU.Vector m Word, a))
+  => MultiPoly v n a
+  -> SV.Vector n (MultiPoly w m a)
+  -> MultiPoly w m a
+subst' = substitute' (scale' 0)
+{-# INLINE subst' #-}
+
+substitute
+  :: forall v u n a b.
+     (G.Vector v (SU.Vector n Word, a), G.Vector u b, Num b)
+  => (a -> b -> b)
+  -> MultiPoly v n a
+  -> SG.Vector u n b
+  -> b
+substitute f (MultiPoly cs) xs = G.foldl' go 0 cs
+  where
+    go :: b -> (SU.Vector n Word, a) -> b
+    go acc (ps, c) = acc + f c (doMonom ps)
+
+    doMonom :: SU.Vector n Word -> b
+    doMonom = SU.ifoldl' (\acc i p -> acc * ((xs `SG.index` i) ^ p)) 1
+{-# INLINE substitute #-}
+
+substitute'
+  :: forall v u n a b.
+     (G.Vector v (SU.Vector n Word, a), G.Vector u b, Semiring b)
+  => (a -> b -> b)
+  -> MultiPoly v n a
+  -> SG.Vector u n b
+  -> b
+substitute' f (MultiPoly cs) xs = G.foldl' go zero cs
+  where
+    go :: b -> (SU.Vector n Word, a) -> b
+    go acc (ps, c) = acc `plus` f c (doMonom ps)
+
+    doMonom :: SU.Vector n Word -> b
+    doMonom = SU.ifoldl' (\acc i p -> acc `times` ((xs `SG.index` i) Semiring.^ p)) one
+{-# INLINE substitute' #-}
+
+-- | Take the derivative of the polynomial with respect to the /i/-th variable.
+--
+-- >>> :set -XDataKinds
+-- >>> deriv 0 (X^3 + 3 * Y) :: UMultiPoly 2 Int
+-- 3 * X^2
+-- >>> deriv 1 (X^3 + 3 * Y) :: UMultiPoly 2 Int
+-- 3
+--
+-- @since 0.5.0.0
+deriv
+  :: (Eq a, Num a, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+deriv i (MultiPoly xs) = MultiPoly $ derivPoly
+  (/= 0)
+  (\ps -> ps SU.// [(i, ps `SU.index` i - 1)])
+  (\ps c -> fromIntegral (ps `SU.index` i) * c)
+  xs
+{-# INLINE deriv #-}
+
+deriv'
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+deriv' i (MultiPoly xs) = MultiPoly $ derivPoly
+  (/= zero)
+  (\ps -> ps SU.// [(i, ps `SU.index` i - 1)])
+  (\ps c -> fromNatural (fromIntegral (ps `SU.index` i)) `times` c)
+  xs
+{-# INLINE deriv' #-}
+
+-- | Compute an indefinite integral of the polynomial
+-- with respect to the /i/-th variable,
+-- setting the constant term to zero.
+--
+-- >>> :set -XDataKinds
+-- >>> integral 0 (3 * X^2 + 2 * Y) :: UMultiPoly 2 Double
+-- 1.0 * X^3 + 2.0 * X * Y
+-- >>> integral 1 (3 * X^2 + 2 * Y) :: UMultiPoly 2 Double
+-- 3.0 * X^2 * Y + 1.0 * Y^2
+--
+-- @since 0.5.0.0
+integral
+  :: (Fractional a, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+integral i (MultiPoly xs)
+  = MultiPoly
+  $ G.map (\(ps, c) -> let p = ps `SU.index` i in
+    (ps SU.// [(i, p + 1)], c / fromIntegral (p + 1))) xs
+{-# INLINE integral #-}
+
+integral'
+  :: (Field a, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+integral' i (MultiPoly xs)
+  = MultiPoly
+  $ G.map (\(ps, c) -> let p = ps `SU.index` i in
+    (ps SU.// [(i, p + 1)], c `quot` Semiring.fromIntegral (p + 1))) xs
+{-# INLINE integral' #-}
+
+-- | Create a polynomial equal to the first variable.
+--
+-- @since 0.5.0.0
+pattern X
+  :: (Eq a, Num a, KnownNat n, 1 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern X <- (isVar 0 -> True)
+  where X = var 0
+
+pattern X'
+  :: (Eq a, Semiring a, KnownNat n, 1 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern X' <- (isVar' 0 -> True)
+  where X' = var' 0
+
+-- | Create a polynomial equal to the second variable.
+--
+-- @since 0.5.0.0
+pattern Y
+  :: (Eq a, Num a, KnownNat n, 2 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern Y <- (isVar 1 -> True)
+  where Y = var 1
+
+pattern Y'
+  :: (Eq a, Semiring a, KnownNat n, 2 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern Y' <- (isVar' 1 -> True)
+  where Y' = var' 1
+
+-- | Create a polynomial equal to the third variable.
+--
+-- @since 0.5.0.0
+pattern Z
+  :: (Eq a, Num a, KnownNat n, 3 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern Z <- (isVar 2 -> True)
+  where Z = var 2
+
+pattern Z'
+  :: (Eq a, Semiring a, KnownNat n, 3 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern Z' <- (isVar' 2 -> True)
+  where Z' = var' 2
+
+var
+  :: forall v n a.
+     (Eq a, Num a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+var i
+  | (1 :: a) == 0 = MultiPoly G.empty
+  | otherwise     = MultiPoly $ G.singleton
+    (SU.generate (\j -> if i == j then 1 else 0), 1)
+{-# INLINE var #-}
+
+var'
+  :: forall v n a.
+     (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+var' i
+  | (one :: a) == zero = MultiPoly G.empty
+  | otherwise          = MultiPoly $ G.singleton
+    (SU.generate (\j -> if i == j then 1 else 0), one)
+{-# INLINE var' #-}
+
+isVar
+  :: forall v n a.
+     (Eq a, Num a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> Bool
+isVar i (MultiPoly xs)
+  | (1 :: a) == 0 = G.null xs
+  | otherwise     = G.length xs == 1 && G.unsafeHead xs == (SU.generate (\j -> if i == j then 1 else 0), 1)
+{-# INLINE isVar #-}
+
+isVar'
+  :: forall v n a.
+     (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> Bool
+isVar' i (MultiPoly xs)
+  | (one :: a) == zero = G.null xs
+  | otherwise          = G.length xs == 1 && G.unsafeHead xs == (SU.generate (\j -> if i == j then 1 else 0), one)
+{-# INLINE isVar' #-}
+
+-------------------------------------------------------------------------------
+
+groupOn :: (G.Vector v a, Eq b) => (a -> b) -> v a -> [v a]
+groupOn f = go
+  where
+    go xs
+      | G.null xs = []
+      | otherwise = case mk of
+        Nothing -> [xs]
+        Just k  -> G.unsafeTake (k + 1) xs : go (G.unsafeDrop (k + 1) xs)
+        where
+          fy = f (G.unsafeHead xs)
+          mk = G.findIndex ((/= fy) . f) (G.unsafeTail xs)
+
+-- | Interpret a multivariate polynomial over 1+/m/ variables
+-- as a univariate polynomial, whose coefficients are
+-- multivariate polynomials over the last /m/ variables.
+--
+-- @since 0.5.0.0
+segregate
+  :: (G.Vector v (SU.Vector (1 + m) Word, a), G.Vector v (SU.Vector m Word, a))
+  => MultiPoly v (1 + m) a
+  -> VPoly (MultiPoly v m a)
+segregate
+  = MultiPoly
+  . G.fromList
+  . map (\vs -> (SU.take (fst (G.unsafeHead vs)), MultiPoly $ G.map (first SU.tail) vs))
+  . groupOn (SU.head . fst)
+  . unMultiPoly
+
+-- | Interpret a univariate polynomials, whose coefficients are
+-- multivariate polynomials over the first /m/ variables,
+-- as a multivariate polynomial over 1+/m/ variables.
+--
+-- @since 0.5.0.0
+unsegregate
+  :: (G.Vector v (SU.Vector (1 + m) Word, a), G.Vector v (SU.Vector m Word, a))
+  => VPoly (MultiPoly v m a)
+  -> MultiPoly v (1 + m) a
+unsegregate
+  = MultiPoly
+  . G.concat
+  . G.toList
+  . G.map (\(v, MultiPoly vs) -> G.map (first (v SU.++)) vs)
+  . unMultiPoly
diff --git a/src/Data/Poly/Internal/Multi/Core.hs b/src/Data/Poly/Internal/Multi/Core.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Multi/Core.hs
@@ -0,0 +1,309 @@
+-- |
+-- Module:      Data.Poly.Internal.Multi.Core
+-- Copyright:   (c) 2019 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Sparse polynomials of one variable.
+--
+
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+module Data.Poly.Internal.Multi.Core
+  ( normalize
+  , plusPoly
+  , minusPoly
+  , convolution
+  , scaleInternal
+  , derivPoly
+  ) where
+
+import Control.Monad
+import Control.Monad.ST
+import Data.Bits
+import Data.Ord
+import qualified Data.Vector.Algorithms.Tim as Tim
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Mutable as MG
+import qualified Data.Vector.Unboxed as U
+
+normalize
+  :: (G.Vector v (t, a), Ord t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> v (t, a)
+  -> v (t, a)
+normalize p add vs
+  | G.null vs = vs
+  | otherwise = runST $ do
+    ws <- G.thaw vs
+    l' <- normalizeM p add ws
+    G.unsafeFreeze $ MG.unsafeSlice 0 l' ws
+{-# INLINABLE normalize #-}
+
+normalizeM
+  :: (G.Vector v (t, a), Ord t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> G.Mutable v s (t, a)
+  -> ST s Int
+normalizeM p add ws = do
+    let l = MG.length ws
+    let go i j acc@(accP, accC)
+          | j >= l =
+            if p accC
+              then do
+                MG.write ws i acc
+                pure $ i + 1
+              else pure i
+          | otherwise = do
+            v@(vp, vc) <- MG.unsafeRead ws j
+            if vp == accP
+              then go i (j + 1) (accP, accC `add` vc)
+              else if p accC
+                then do
+                  MG.write ws i acc
+                  go (i + 1) (j + 1) v
+                else go i (j + 1) v
+    Tim.sortBy (comparing fst) ws
+    wsHead <- MG.unsafeRead ws 0
+    go 0 1 wsHead
+{-# INLINABLE normalizeM #-}
+
+plusPoly
+  :: (G.Vector v (t, a), Ord t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> v (t, a)
+  -> v (t, a)
+  -> v (t, a)
+plusPoly p add = \xs ys -> runST $ do
+  zs <- MG.unsafeNew (G.length xs + G.length ys)
+  lenZs <- plusPolyM p add xs ys zs
+  G.unsafeFreeze $ MG.unsafeSlice 0 lenZs zs
+{-# INLINABLE plusPoly #-}
+
+plusPolyM
+  :: (G.Vector v (t, a), Ord t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> v (t, a)
+  -> v (t, a)
+  -> G.Mutable v s (t, a)
+  -> ST s Int
+plusPolyM p add xs ys zs = go 0 0 0
+  where
+    lenXs = G.length xs
+    lenYs = G.length ys
+
+    go ix iy iz
+      | ix == lenXs, iy == lenYs = pure iz
+      | ix == lenXs = do
+        G.unsafeCopy
+          (MG.unsafeSlice iz (lenYs - iy) zs)
+          (G.unsafeSlice iy (lenYs - iy) ys)
+        pure $ iz + lenYs - iy
+      | iy == lenYs = do
+        G.unsafeCopy
+          (MG.unsafeSlice iz (lenXs - ix) zs)
+          (G.unsafeSlice ix (lenXs - ix) xs)
+        pure $ iz + lenXs - ix
+      | (xp, xc) <- G.unsafeIndex xs ix
+      , (yp, yc) <- G.unsafeIndex ys iy
+      = case xp `compare` yp of
+        LT -> do
+          MG.unsafeWrite zs iz (xp, xc)
+          go (ix + 1) iy (iz + 1)
+        EQ -> do
+          let zc = xc `add` yc
+          if p zc then do
+            MG.unsafeWrite zs iz (xp, zc)
+            go (ix + 1) (iy + 1) (iz + 1)
+          else
+            go (ix + 1) (iy + 1) iz
+        GT -> do
+          MG.unsafeWrite zs iz (yp, yc)
+          go ix (iy + 1) (iz + 1)
+{-# INLINE plusPolyM #-}
+
+minusPoly
+  :: (G.Vector v (t, a), Ord t)
+  => (a -> Bool)
+  -> (a -> a)
+  -> (a -> a -> a)
+  -> v (t, a)
+  -> v (t, a)
+  -> v (t, a)
+minusPoly p neg sub = \xs ys -> runST $ do
+  let lenXs = G.length xs
+      lenYs = G.length ys
+  zs <- MG.unsafeNew (lenXs + lenYs)
+  let go ix iy iz
+        | ix == lenXs, iy == lenYs = pure iz
+        | ix == lenXs = do
+          forM_ [iy .. lenYs - 1] $ \i ->
+            MG.unsafeWrite zs (iz + i - iy)
+              (fmap neg (G.unsafeIndex ys i))
+          pure $ iz + lenYs - iy
+        | iy == lenYs = do
+          G.unsafeCopy
+            (MG.unsafeSlice iz (lenXs - ix) zs)
+            (G.unsafeSlice ix (lenXs - ix) xs)
+          pure $ iz + lenXs - ix
+        | (xp, xc) <- G.unsafeIndex xs ix
+        , (yp, yc) <- G.unsafeIndex ys iy
+        = case xp `compare` yp of
+          LT -> do
+            MG.unsafeWrite zs iz (xp, xc)
+            go (ix + 1) iy (iz + 1)
+          EQ -> do
+            let zc = xc `sub` yc
+            if p zc then do
+              MG.unsafeWrite zs iz (xp, zc)
+              go (ix + 1) (iy + 1) (iz + 1)
+            else
+              go (ix + 1) (iy + 1) iz
+          GT -> do
+            MG.unsafeWrite zs iz (yp, neg yc)
+            go ix (iy + 1) (iz + 1)
+  lenZs <- go 0 0 0
+  G.unsafeFreeze $ MG.unsafeSlice 0 lenZs zs
+{-# INLINABLE minusPoly #-}
+
+scaleM
+  :: (G.Vector v (t, a), Num t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> v (t, a)
+  -> (t, a)
+  -> G.Mutable v s (t, a)
+  -> ST s Int
+scaleM p mul xs (yp, yc) zs = go 0 0
+  where
+    lenXs = G.length xs
+
+    go ix iz
+      | ix == lenXs = pure iz
+      | (xp, xc) <- G.unsafeIndex xs ix
+      = do
+        let zc = xc `mul` yc
+        if p zc then do
+          MG.unsafeWrite zs iz (xp + yp, zc)
+          go (ix + 1) (iz + 1)
+        else
+          go (ix + 1) iz
+{-# INLINABLE scaleM #-}
+
+scaleInternal
+  :: (G.Vector v (t, a), Num t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> t
+  -> a
+  -> v (t, a)
+  -> v (t, a)
+scaleInternal p mul yp yc xs = runST $ do
+  zs <- MG.unsafeNew (G.length xs)
+  len <- scaleM p (flip mul) xs (yp, yc) zs
+  G.unsafeFreeze $ MG.unsafeSlice 0 len zs
+{-# INLINABLE scaleInternal #-}
+
+convolution
+  :: forall v t a.
+     (G.Vector v (t, a), Ord t, Num t)
+  => (a -> Bool)
+  -> (a -> a -> a)
+  -> (a -> a -> a)
+  -> v (t, a)
+  -> v (t, a)
+  -> v (t, a)
+convolution p add mult = \xs ys ->
+  if G.length xs >= G.length ys
+  then go mult xs ys
+  else go (flip mult) ys xs
+  where
+    go :: (a -> a -> a) -> v (t, a) -> v (t, a) -> v (t, a)
+    go mul long short = runST $ do
+      let lenLong   = G.length long
+          lenShort  = G.length short
+          lenBuffer = lenLong * lenShort
+      slices <- MG.unsafeNew lenShort
+      buffer <- MG.unsafeNew lenBuffer
+
+      forM_ [0 .. lenShort - 1] $ \iShort -> do
+        let (pShort, cShort) = G.unsafeIndex short iShort
+            from = iShort * lenLong
+            bufferSlice = MG.unsafeSlice from lenLong buffer
+        len <- scaleM p mul long (pShort, cShort) bufferSlice
+        MG.unsafeWrite slices iShort (from, len)
+
+      slices' <- G.unsafeFreeze slices
+      buffer' <- G.unsafeFreeze buffer
+      bufferNew <- MG.unsafeNew lenBuffer
+      gogo slices' buffer' bufferNew
+
+    gogo
+      :: U.Vector (Int, Int)
+      -> v (t, a)
+      -> G.Mutable v s (t, a)
+      -> ST s (v (t, a))
+    gogo slices buffer bufferNew
+      | G.length slices == 0
+      = pure G.empty
+      | G.length slices == 1
+      , (from, len) <- G.unsafeIndex slices 0
+      = pure $ G.unsafeSlice from len buffer
+      | otherwise = do
+        let nSlices = G.length slices
+        slicesNew <- MG.unsafeNew ((nSlices + 1) `shiftR` 1)
+        forM_ [0 .. (nSlices - 2) `shiftR` 1] $ \i -> do
+          let (from1, len1) = G.unsafeIndex slices (2 * i)
+              (from2, len2) = G.unsafeIndex slices (2 * i + 1)
+              slice1 = G.unsafeSlice from1 len1 buffer
+              slice2 = G.unsafeSlice from2 len2 buffer
+              slice3 = MG.unsafeSlice from1 (len1 + len2) bufferNew
+          len3 <- plusPolyM p add slice1 slice2 slice3
+          MG.unsafeWrite slicesNew i (from1, len3)
+
+        when (odd nSlices) $ do
+          let (from, len) = G.unsafeIndex slices (nSlices - 1)
+              slice1 = G.unsafeSlice from len buffer
+              slice3 = MG.unsafeSlice from len bufferNew
+          G.unsafeCopy slice3 slice1
+          MG.unsafeWrite slicesNew (nSlices `shiftR` 1) (from, len)
+
+        slicesNew' <- G.unsafeFreeze slicesNew
+        buffer'    <- G.unsafeThaw   buffer
+        bufferNew' <- G.unsafeFreeze bufferNew
+        gogo slicesNew' bufferNew' buffer'
+{-# INLINABLE convolution #-}
+
+derivPoly
+  :: (G.Vector v (t, a))
+  => (a -> Bool)   -- ^ is coefficient non-zero?
+  -> (t -> t)      -- ^ how to modify powers?
+  -> (t -> a -> a) -- ^ how to modify coefficient?
+  -> v (t, a)
+  -> v (t, a)
+derivPoly p dec mul xs
+  | G.null xs = G.empty
+  | otherwise = runST $ do
+    let lenXs = G.length xs
+    zs <- MG.unsafeNew lenXs
+    let go ix iz
+          | ix == lenXs = pure iz
+          | (xp, xc) <- G.unsafeIndex xs ix
+          = do
+            let zc = xp `mul` xc
+            if p zc then do
+              MG.unsafeWrite zs iz (dec xp, zc)
+              go (ix + 1) (iz + 1)
+            else
+              go (ix + 1) iz
+    lenZs <- go 0 0
+    G.unsafeFreeze $ MG.unsafeSlice 0 lenZs zs
+{-# INLINABLE derivPoly #-}
diff --git a/src/Data/Poly/Internal/Multi/Field.hs b/src/Data/Poly/Internal/Multi/Field.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Multi/Field.hs
@@ -0,0 +1,75 @@
+-- |
+-- Module:      Data.Poly.Internal.Multi.Field
+-- Copyright:   (c) 2019 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- 'Euclidean' instance with a 'Field' constraint on the coefficient type.
+--
+
+{-# LANGUAGE ConstraintKinds            #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Data.Poly.Internal.Multi.Field
+  ( quotRemFractional
+  ) where
+
+import Prelude hiding (quotRem, quot, rem, div, gcd)
+import Control.Arrow
+import Control.Exception
+import Data.Euclidean (Euclidean(..), Field)
+import Data.Semiring (Semiring(..), minus)
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed.Sized as SU
+
+import Data.Poly.Internal.Multi
+import Data.Poly.Internal.Multi.GcdDomain ()
+
+-- | Note that 'degree' 0 = 0.
+instance (Eq a, Field a, G.Vector v (SU.Vector 1 Word, a)) => Euclidean (Poly v a) where
+  degree (MultiPoly xs)
+    | G.null xs = 0
+    | otherwise = fromIntegral (SU.head (fst (G.unsafeLast xs)))
+
+  quotRem = quotientRemainder zero plus minus times quot
+
+-- | Polynomial division with remainder.
+--
+-- >>> quotRemFractional (X^3 + 2) (X^2 - 1 :: UPoly Double)
+-- (1.0 * X,1.0 * X + 2.0)
+--
+-- @since 0.5.0.0
+quotRemFractional :: (Eq a, Fractional a, G.Vector v (SU.Vector 1 Word, a)) => Poly v a -> Poly v a -> (Poly v a, Poly v a)
+quotRemFractional = quotientRemainder 0 (+) (-) (*) (/)
+{-# INLINE quotRemFractional #-}
+
+quotientRemainder
+  :: G.Vector v (SU.Vector 1 Word, a)
+  => Poly v a                           -- ^ zero
+  -> (Poly v a -> Poly v a -> Poly v a) -- ^ add
+  -> (Poly v a -> Poly v a -> Poly v a) -- ^ subtract
+  -> (Poly v a -> Poly v a -> Poly v a) -- ^ multiply
+  -> (a -> a -> a)                      -- ^ divide
+  -> Poly v a                           -- ^ dividend
+  -> Poly v a                           -- ^ divisor
+  -> (Poly v a, Poly v a)
+quotientRemainder zer add sub mul div ts ys = case leading ys of
+  Nothing -> throw DivideByZero
+  Just (yp, yc) -> go ts
+    where
+      go xs = case leading xs of
+        Nothing -> (zer, zer)
+        Just (xp, xc) -> case xp `compare` yp of
+          LT -> (zer, xs)
+          EQ -> (zs, xs')
+          GT -> first (`add` zs) $ go xs'
+          where
+            zs = MultiPoly $ G.singleton (SU.singleton (xp - yp), xc `div` yc)
+            xs' = xs `sub` (zs `mul` ys)
diff --git a/src/Data/Poly/Internal/Multi/GcdDomain.hs b/src/Data/Poly/Internal/Multi/GcdDomain.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Multi/GcdDomain.hs
@@ -0,0 +1,168 @@
+-- |
+-- Module:      Data.Poly.Internal.Multi.GcdDomain
+-- Copyright:   (c) 2019 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- 'GcdDomain' instance with a 'GcdDomain' constraint on the coefficient type.
+--
+
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE GADTs                      #-}
+{-# LANGUAGE QuantifiedConstraints      #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeOperators              #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Data.Poly.Internal.Multi.GcdDomain
+  () where
+
+import Prelude hiding (gcd, lcm, (^))
+import Control.Exception
+import Data.Euclidean
+import Data.Maybe
+import Data.Proxy
+import Data.Semiring (Semiring(..), Ring(), minus)
+import Data.Type.Equality
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed.Sized as SU
+import GHC.TypeNats (KnownNat, type (+), SomeNat(..), natVal, sameNat, someNatVal)
+import Unsafe.Coerce
+
+import Data.Poly.Internal.Multi
+
+instance {-# OVERLAPPING #-} (Eq a, Ring a, GcdDomain a, G.Vector v (SU.Vector 1 Word, a)) => GcdDomain (Poly v a) where
+  divide xs ys
+    | G.null (unMultiPoly ys) = throw DivideByZero
+    | G.length (unMultiPoly ys) == 1 = divideSingleton xs (G.unsafeHead (unMultiPoly ys))
+    | otherwise = divide1 xs ys
+
+  gcd xs ys
+    | G.null (unMultiPoly xs) = ys
+    | G.null (unMultiPoly ys) = xs
+    | G.length (unMultiPoly xs) == 1 = gcdSingleton (G.unsafeHead (unMultiPoly xs)) ys
+    | G.length (unMultiPoly ys) == 1 = gcdSingleton (G.unsafeHead (unMultiPoly ys)) xs
+    | otherwise = gcd1 xs ys
+
+  lcm xs ys
+    | G.null (unMultiPoly xs) || G.null (unMultiPoly ys) = zero
+    | otherwise = (xs `divide'` gcd xs ys) `times` ys
+
+  coprime x y = isJust (one `divide` gcd x y)
+
+data IsSucc n where
+  IsSucc :: KnownNat m => n :~: 1 + m -> IsSucc n
+
+-- | This is unsafe when n ~ 0.
+isSucc :: forall n. KnownNat n => IsSucc n
+isSucc = case someNatVal (natVal (Proxy :: Proxy n) - 1) of
+  SomeNat (_ :: Proxy m) -> IsSucc (unsafeCoerce Refl :: n :~: 1 + m)
+
+instance (Eq a, Ring a, GcdDomain a, KnownNat n, forall m. KnownNat m => G.Vector v (SU.Vector m Word, a), forall m. KnownNat m => Eq (v (SU.Vector m Word, a))) => GcdDomain (MultiPoly v n a) where
+  divide xs ys
+    | G.null (unMultiPoly ys) = throw DivideByZero
+    | G.length (unMultiPoly ys) == 1 = divideSingleton xs (G.unsafeHead (unMultiPoly ys))
+    -- Polynomials of zero variables are necessarily constants,
+    -- so they have been dealt with above.
+    | Just Refl <- sameNat (Proxy :: Proxy n) (Proxy :: Proxy 1)
+    = divide1 xs ys
+    | otherwise = case isSucc :: IsSucc n of
+      IsSucc Refl -> unsegregate <$> segregate xs `divide` segregate ys
+  gcd xs ys
+    | G.null (unMultiPoly xs) = ys
+    | G.null (unMultiPoly ys) = xs
+    | G.length (unMultiPoly xs) == 1 = gcdSingleton (G.unsafeHead (unMultiPoly xs)) ys
+    | G.length (unMultiPoly ys) == 1 = gcdSingleton (G.unsafeHead (unMultiPoly ys)) xs
+    -- Polynomials of zero variables are necessarily constants,
+    -- so they have been dealt with above.
+    | Just Refl <- sameNat (Proxy :: Proxy n) (Proxy :: Proxy 1)
+    = gcd1 xs ys
+    | otherwise = case isSucc :: IsSucc n of
+      IsSucc Refl -> unsegregate $ segregate xs `gcd` segregate ys
+
+divideSingleton
+  :: (GcdDomain a, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+  -> (SU.Vector n Word, a)
+  -> Maybe (MultiPoly v n a)
+divideSingleton (MultiPoly pcs) (p, c) = MultiPoly <$> G.mapM divideMonomial pcs
+  where
+    divideMonomial (p', c')
+      | SU.and (SU.zipWith (>=) p' p)
+      , Just c'' <- c' `divide` c
+      = Just (SU.zipWith (-) p' p, c'')
+      | otherwise
+      = Nothing
+
+gcdSingleton
+  :: (Eq a, GcdDomain a, G.Vector v (SU.Vector n Word, a))
+  => (SU.Vector n Word, a)
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+gcdSingleton pc (MultiPoly pcs) = uncurry monomial' $
+  G.foldl' (\(accP, accC) (p, c) -> (SU.zipWith min accP p, gcd accC c)) pc pcs
+
+divide1
+  :: (Eq a, GcdDomain a, Ring a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+  -> Maybe (Poly v a)
+divide1 xs ys = case leading ys of
+  Nothing -> throw DivideByZero
+  Just (yp, yc) -> case leading xs of
+    Nothing -> Just xs
+    Just (xp, xc)
+      | xp < yp -> Nothing
+      | otherwise -> do
+        zc <- divide xc yc
+        let z = MultiPoly $ G.singleton (SU.singleton (xp - yp), zc)
+        rest <- divide1 (xs `minus` z `times` ys) ys
+        pure $ rest `plus` z
+
+gcd1
+  :: (Eq a, GcdDomain a, Ring a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+  -> Poly v a
+gcd1 x@(MultiPoly xs) y@(MultiPoly ys) =
+  times xy (divide1' z (monomial' 0 (content zs)))
+  where
+    z@(MultiPoly zs) = gcdHelper x y
+    xy = monomial' 0 (gcd (content xs) (content ys))
+    divide1' = (fromMaybe (error "gcd: violated internal invariant") .) . divide1
+
+content :: (GcdDomain a, G.Vector v (t, a)) => v (t, a) -> a
+content = G.foldl' (\acc (_, t) -> gcd acc t) zero
+
+gcdHelper
+  :: (Eq a, Ring a, GcdDomain a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+  -> Poly v a
+gcdHelper xs ys = case (leading xs, leading ys) of
+  (Nothing, _) -> ys
+  (_, Nothing) -> xs
+  (Just (xp, xc), Just (yp, yc))
+    | yp <= xp
+    , Just xy <- xc `divide` yc
+    -> gcdHelper ys (xs `minus` ys `times` monomial' (SU.singleton (xp - yp)) xy)
+    | xp <= yp
+    , Just yx <- yc `divide` xc
+    -> gcdHelper xs (ys `minus` xs `times` monomial' (SU.singleton (yp - xp)) yx)
+    | yp <= xp
+    -> gcdHelper ys (xs `times` monomial' 0 gx `minus` ys `times` monomial' (SU.singleton (xp - yp)) gy)
+    | otherwise
+    -> gcdHelper xs (ys `times` monomial' 0 gy `minus` xs `times` monomial' (SU.singleton (yp - xp)) gx)
+    where
+      g = lcm xc yc
+      gx = divide' g xc
+      gy = divide' g yc
+
+divide' :: GcdDomain a => a -> a -> a
+divide' = (fromMaybe (error "gcd: violated internal invariant") .) . divide
diff --git a/src/Data/Poly/Internal/Multi/Laurent.hs b/src/Data/Poly/Internal/Multi/Laurent.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Internal/Multi/Laurent.hs
@@ -0,0 +1,553 @@
+-- |
+-- Module:      Data.Poly.Internal.Multi.Laurent
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Sparse multivariate
+-- <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>.
+--
+
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE PatternSynonyms            #-}
+{-# LANGUAGE QuantifiedConstraints      #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE StandaloneDeriving         #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE TypeOperators              #-}
+{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE ViewPatterns               #-}
+
+module Data.Poly.Internal.Multi.Laurent
+  ( MultiLaurent
+  , VMultiLaurent
+  , UMultiLaurent
+  , unMultiLaurent
+  , toMultiLaurent
+  , leading
+  , monomial
+  , scale
+  , pattern X
+  , pattern Y
+  , pattern Z
+  , (^-)
+  , eval
+  , subst
+  , deriv
+  -- * Univariate polynomials
+  , Laurent
+  , VLaurent
+  , ULaurent
+  , unLaurent
+  , toLaurent
+  -- * Conversions
+  , segregate
+  , unsegregate
+  ) where
+
+import Prelude hiding (quotRem, quot, rem, gcd, lcm)
+import Control.Arrow (first)
+import Control.DeepSeq (NFData(..))
+import Control.Exception
+import Data.Euclidean (GcdDomain(..), Euclidean(..), Field)
+import Data.Finite
+import Data.Kind
+import Data.List (intersperse, foldl1')
+import Data.Semiring (Semiring(..), Ring())
+import qualified Data.Semiring as Semiring
+import qualified Data.Vector as V
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Sized as SG
+import qualified Data.Vector.Sized as SV
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Sized as SU
+import GHC.Exts
+import GHC.TypeNats (KnownNat, Nat, type (+), type (<=))
+
+import Data.Poly.Internal.Multi.Core (derivPoly)
+import Data.Poly.Internal.Multi (Poly, MultiPoly(..))
+import qualified Data.Poly.Internal.Multi as Multi
+import Data.Poly.Internal.Multi.Field ()
+import Data.Poly.Internal.Multi.GcdDomain ()
+
+-- | Sparse
+-- <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>
+-- of @n@ variables with coefficients from @a@,
+-- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
+--
+-- Use the patterns 'X', 'Y', 'Z' and the '^-' operator for construction:
+--
+-- >>> (X + 1) + (Y^-1 - 1) :: VMultiLaurent 2 Integer
+-- 1 * X + 1 * Y^-1
+-- >>> (X + 1) * (Z - X^-1) :: UMultiLaurent 3 Int
+-- 1 * X * Z + 1 * Z + (-1) + (-1) * X^-1
+--
+-- Polynomials are stored normalized, without
+-- zero coefficients, so 0 * X + 1 + 0 * X^-1 equals to 1.
+--
+-- The 'Ord' instance does not make much sense mathematically,
+-- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
+--
+-- Due to being polymorphic by multiple axis, the performance of `MultiLaurent` crucially
+-- depends on specialisation of instances. Clients are strongly recommended
+-- to compile with @ghc-options:@ @-fspecialise-aggressively@ and suggested to enable @-O2@.
+--
+-- @since 0.5.0.0
+data MultiLaurent (v :: Type -> Type) (n :: Nat) (a :: Type) =
+  MultiLaurent !(SU.Vector n Int) !(MultiPoly v n a)
+
+deriving instance Eq  (v (SU.Vector n Word, a)) => Eq  (MultiLaurent v n a)
+deriving instance Ord (v (SU.Vector n Word, a)) => Ord (MultiLaurent v n a)
+
+-- | Multivariate Laurent polynomials backed by boxed vectors.
+--
+-- @since 0.5.0.0
+type VMultiLaurent (n :: Nat) (a :: Type) = MultiLaurent V.Vector n a
+
+-- | Multivariate Laurent polynomials backed by unboxed vectors.
+--
+-- @since 0.5.0.0
+type UMultiLaurent (n :: Nat) (a :: Type) = MultiLaurent U.Vector n a
+
+-- | <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>
+-- of one variable with coefficients from @a@,
+-- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
+--
+-- Use the pattern 'X' and the '^-' operator for construction:
+--
+-- >>> (X + 1) + (X^-1 - 1) :: VLaurent Integer
+-- 1 * X + 1 * X^-1
+-- >>> (X + 1) * (1 - X^-1) :: ULaurent Int
+-- 1 * X + (-1) * X^-1
+--
+-- Polynomials are stored normalized, without
+-- zero coefficients, so 0 * X + 1 + 0 * X^-1 equals to 1.
+--
+-- The 'Ord' instance does not make much sense mathematically,
+-- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
+--
+-- Due to being polymorphic by multiple axis, the performance of `Laurent` crucially
+-- depends on specialisation of instances. Clients are strongly recommended
+-- to compile with @ghc-options:@ @-fspecialise-aggressively@ and suggested to enable @-O2@.
+--
+-- @since 0.4.0.0
+type Laurent (v :: Type -> Type) (a :: Type) = MultiLaurent v 1 a
+
+-- | Laurent polynomials backed by boxed vectors.
+--
+-- @since 0.4.0.0
+type VLaurent (a :: Type) = Laurent V.Vector a
+
+-- | Laurent polynomials backed by unboxed vectors.
+--
+-- @since 0.4.0.0
+type ULaurent (a :: Type) = Laurent U.Vector a
+
+instance (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Int, a), G.Vector v (SU.Vector n Word, a)) => IsList (MultiLaurent v n a) where
+  type Item (MultiLaurent v n a) = (SU.Vector n Int, a)
+
+  fromList [] = MultiLaurent 0 zero
+  fromList xs = toMultiLaurent minPow (fromList ys)
+    where
+      minPow = foldl1' (SU.zipWith min) (map fst xs)
+      ys = map (first (SU.map fromIntegral . subtract minPow)) xs
+
+  toList (MultiLaurent off (MultiPoly poly)) =
+    map (first ((+ off) . SU.map fromIntegral)) $ G.toList poly
+
+-- | Deconstruct a 'MultiLaurent' polynomial into an offset (largest possible)
+-- and a regular polynomial.
+--
+-- >>> unMultiLaurent (2 * X + 1 :: UMultiLaurent 2 Int)
+-- (Vector [0,0],2 * X + 1)
+-- >>> unMultiLaurent (1 + 2 * X^-1 :: UMultiLaurent 2 Int)
+-- (Vector [-1,0],1 * X + 2)
+-- >>> unMultiLaurent (2 * X^2 + X :: UMultiLaurent 2 Int)
+-- (Vector [1,0],2 * X + 1)
+-- >>> unMultiLaurent (0 :: UMultiLaurent 2 Int)
+-- (Vector [0,0],0)
+--
+-- @since 0.5.0.0
+unMultiLaurent :: MultiLaurent v n a -> (SU.Vector n Int, MultiPoly v n a)
+unMultiLaurent (MultiLaurent off poly) = (off, poly)
+
+-- | Deconstruct a 'Laurent' polynomial into an offset (largest possible)
+-- and a regular polynomial.
+--
+-- >>> unLaurent (2 * X + 1 :: ULaurent Int)
+-- (0,2 * X + 1)
+-- >>> unLaurent (1 + 2 * X^-1 :: ULaurent Int)
+-- (-1,1 * X + 2)
+-- >>> unLaurent (2 * X^2 + X :: ULaurent Int)
+-- (1,2 * X + 1)
+-- >>> unLaurent (0 :: ULaurent Int)
+-- (0,0)
+--
+-- @since 0.4.0.0
+unLaurent :: Laurent v a -> (Int, Poly v a)
+unLaurent = first SU.head . unMultiLaurent
+
+-- | Construct a 'MultiLaurent' polynomial from an offset and a regular polynomial.
+-- One can imagine it as 'Data.Poly.Multi.Semiring.scale', but allowing negative offsets.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> toMultiLaurent (fromTuple (2, 0)) (2 * Data.Poly.Multi.X + 1) :: UMultiLaurent 2 Int
+-- 2 * X^3 + 1 * X^2
+-- >>> toMultiLaurent (fromTuple (0, -2)) (2 * Data.Poly.Multi.X + 1) :: UMultiLaurent 2 Int
+-- 2 * X * Y^-2 + 1 * Y^-2
+toMultiLaurent
+  :: (KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Int
+  -> MultiPoly v n a
+  -> MultiLaurent v n a
+toMultiLaurent off (MultiPoly xs)
+  | G.null xs = MultiLaurent 0 (MultiPoly G.empty)
+  | otherwise = MultiLaurent (SU.zipWith (\o m -> o + fromIntegral m) off minPow) (MultiPoly ys)
+    where
+      minPow = G.foldl'(\acc (x, _) -> SU.zipWith min acc x) (SU.replicate maxBound) xs
+      ys
+        | SU.all (== 0) minPow = xs
+        | otherwise = G.map (first (SU.zipWith subtract minPow)) xs
+{-# INLINE toMultiLaurent #-}
+
+-- | Construct a 'Laurent' polynomial from an offset and a regular polynomial.
+-- One can imagine it as 'Data.Poly.Sparse.Semiring.scale', but allowing negative offsets.
+--
+-- >>> toLaurent 2 (2 * Data.Poly.Sparse.X + 1) :: ULaurent Int
+-- 2 * X^3 + 1 * X^2
+-- >>> toLaurent (-2) (2 * Data.Poly.Sparse.X + 1) :: ULaurent Int
+-- 2 * X^-1 + 1 * X^-2
+--
+-- @since 0.4.0.0
+toLaurent
+  :: G.Vector v (SU.Vector 1 Word, a)
+  => Int
+  -> Poly v a
+  -> Laurent v a
+toLaurent = toMultiLaurent . SU.singleton
+{-# INLINABLE toLaurent #-}
+
+instance NFData (v (SU.Vector n Word, a)) => NFData (MultiLaurent v n a) where
+  rnf (MultiLaurent off poly) = rnf off `seq` rnf poly
+
+instance (Show a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Show (MultiLaurent v n a) where
+  showsPrec d (MultiLaurent off (MultiPoly xs))
+    | G.null xs
+      = showString "0"
+    | otherwise
+      = showParen (d > 0)
+      $ foldl (.) id
+      $ intersperse (showString " + ")
+      $ G.foldl (\acc (is, c) -> showCoeff (SU.map fromIntegral is + off) c : acc) [] xs
+    where
+      showCoeff is c
+        = showsPrec 7 c . foldl (.) id
+        ( map ((showString " * " .) . uncurry showPower)
+        $ filter ((/= 0) . fst)
+        $ zip (SU.toList is) (finites :: [Finite n]))
+
+      -- Negative powers should be displayed without surrounding brackets
+      showPower :: Int -> Finite n -> String -> String
+      showPower 1 n = showString (showVar n)
+      showPower i n = showString (showVar n) . showString ("^" ++ show i)
+
+      showVar :: Finite n -> String
+      showVar = \case
+        0 -> "X"
+        1 -> "Y"
+        2 -> "Z"
+        k -> "X" ++ show k
+
+-- | Return the leading power and coefficient of a non-zero polynomial.
+--
+-- >>> leading ((2 * X + 1) * (2 * X^2 - 1) :: ULaurent Int)
+-- Just (3,4)
+-- >>> leading (0 :: ULaurent Int)
+-- Nothing
+--
+-- @since 0.4.0.0
+leading :: G.Vector v (SU.Vector 1 Word, a) => Laurent v a -> Maybe (Int, a)
+leading (MultiLaurent off poly) = first ((+ SU.head off) . fromIntegral) <$> Multi.leading poly
+
+-- | Note that 'abs' = 'id' and 'signum' = 'const' 1.
+instance (Eq a, Num a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Num (MultiLaurent v n a) where
+  MultiLaurent off1 poly1 * MultiLaurent off2 poly2 = toMultiLaurent (off1 + off2) (poly1 * poly2)
+  MultiLaurent off1 poly1 + MultiLaurent off2 poly2 = toMultiLaurent off (poly1' + poly2')
+    where
+      off    = SU.zipWith min off1 off2
+      poly1' = Multi.scale (SU.zipWith (\x y -> fromIntegral (x - y)) off1 off) 1 poly1
+      poly2' = Multi.scale (SU.zipWith (\x y -> fromIntegral (x - y)) off2 off) 1 poly2
+  MultiLaurent off1 poly1 - MultiLaurent off2 poly2 = toMultiLaurent off (poly1' - poly2')
+    where
+      off    = SU.zipWith min off1 off2
+      poly1' = Multi.scale (SU.zipWith (\x y -> fromIntegral (x - y)) off1 off) 1 poly1
+      poly2' = Multi.scale (SU.zipWith (\x y -> fromIntegral (x - y)) off2 off) 1 poly2
+  negate (MultiLaurent off poly) = MultiLaurent off (negate poly)
+  abs = id
+  signum = const 1
+  fromInteger n = MultiLaurent 0 (fromInteger n)
+  {-# INLINE (+) #-}
+  {-# INLINE (-) #-}
+  {-# INLINE negate #-}
+  {-# INLINE fromInteger #-}
+  {-# INLINE (*) #-}
+
+instance (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Semiring (MultiLaurent v n a) where
+  zero = MultiLaurent 0 zero
+  one  = MultiLaurent 0 one
+  MultiLaurent off1 poly1 `times` MultiLaurent off2 poly2 =
+    toMultiLaurent (off1 + off2) (poly1 `times` poly2)
+  MultiLaurent off1 poly1 `plus` MultiLaurent off2 poly2 = toMultiLaurent off (poly1' `plus` poly2')
+    where
+      off    = SU.zipWith min off1 off2
+      poly1' = Multi.scale' (SU.zipWith (\x y -> fromIntegral (x - y)) off1 off) one poly1
+      poly2' = Multi.scale' (SU.zipWith (\x y -> fromIntegral (x - y)) off2 off) one poly2
+  fromNatural n = MultiLaurent 0 (fromNatural n)
+  {-# INLINE zero #-}
+  {-# INLINE one #-}
+  {-# INLINE plus #-}
+  {-# INLINE times #-}
+  {-# INLINE fromNatural #-}
+
+instance (Eq a, Ring a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Ring (MultiLaurent v n a) where
+  negate (MultiLaurent off poly) = MultiLaurent off (Semiring.negate poly)
+
+-- | Create a monomial from a power and a coefficient.
+--
+-- @since 0.5.0.0
+monomial
+  :: (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Int
+  -> a
+  -> MultiLaurent v n a
+monomial p c
+  | c == zero = MultiLaurent 0 zero
+  | otherwise = MultiLaurent p (Multi.monomial' 0 c)
+{-# INLINE monomial #-}
+
+-- | Multiply a polynomial by a monomial, expressed as a power and a coefficient.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> scale (fromTuple (1, 1)) 3 (X^-2 + Y) :: UMultiLaurent 2 Int
+-- 3 * X * Y^2 + 3 * X^-1 * Y
+--
+-- @since 0.5.0.0
+scale
+  :: (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Int
+  -> a
+  -> MultiLaurent v n a
+  -> MultiLaurent v n a
+scale yp yc (MultiLaurent off poly) = toMultiLaurent (off + yp) (Multi.scale' 0 yc poly)
+
+-- | Evaluate the polynomial at a given point.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> eval (X^2 + Y^-1 :: UMultiLaurent 2 Double) (fromTuple (3, 4) :: Data.Vector.Sized.Vector 2 Double)
+-- 9.25
+--
+-- @since 0.5.0.0
+eval
+  :: (Field a, G.Vector v (SU.Vector n Word, a), G.Vector u a)
+  => MultiLaurent v n a
+  -> SG.Vector u n a
+  -> a
+eval (MultiLaurent off poly) xs = Multi.eval' poly xs `times`
+  SU.ifoldl' (\acc i o -> acc `times` (let x = SG.index xs i in if o >= 0 then x Semiring.^ o else quot one x Semiring.^ (- o))) one off
+{-# INLINE eval #-}
+
+-- | Substitute another polynomial instead of 'Data.Poly.Multi.X'.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> import Data.Poly.Multi (UMultiPoly)
+-- >>> subst (Data.Poly.Multi.X * Data.Poly.Multi.Y :: UMultiPoly 2 Int) (fromTuple (X + Y^-1, Y + X^-1 :: UMultiLaurent 2 Int))
+-- 1 * X * Y + 2 + 1 * X^-1 * Y^-1
+--
+-- @since 0.5.0.0
+subst
+  :: (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a), G.Vector w (SU.Vector n Word, a))
+  => MultiPoly v n a
+  -> SV.Vector n (MultiLaurent w n a)
+  -> MultiLaurent w n a
+subst = Multi.substitute' (scale 0)
+{-# INLINE subst #-}
+
+-- | Take the derivative of the polynomial with respect to the /i/-th variable.
+--
+-- >>> :set -XDataKinds
+-- >>> deriv 0 (X^3 + 3 * Y) :: UMultiLaurent 2 Int
+-- 3 * X^2
+-- >>> deriv 1 (X^3 + 3 * Y) :: UMultiLaurent 2 Int
+-- 3
+--
+-- @since 0.5.0.0
+deriv
+  :: (Eq a, Ring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiLaurent v n a
+  -> MultiLaurent v n a
+deriv i (MultiLaurent off (MultiPoly xs)) =
+  toMultiLaurent (off SU.// [(i, off `SU.index` i - 1)]) $ MultiPoly $ derivPoly
+    (/= zero)
+    id
+    (\ps c -> Semiring.fromIntegral (fromIntegral (ps `SU.index` i) + off `SU.index` i) `times` c)
+    xs
+{-# INLINE deriv #-}
+
+-- | Create a polynomial equal to the first variable.
+--
+-- @since 0.5.0.0
+pattern X
+  :: (Eq a, Semiring a, KnownNat n, 1 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiLaurent v n a
+pattern X <- (isVar 0 -> True)
+  where X = var 0
+
+-- | Create a polynomial equal to the second variable.
+--
+-- @since 0.5.0.0
+pattern Y
+  :: (Eq a, Semiring a, KnownNat n, 2 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiLaurent v n a
+pattern Y <- (isVar 1 -> True)
+  where Y = var 1
+
+-- | Create a polynomial equal to the third variable.
+--
+-- @since 0.5.0.0
+pattern Z
+  :: (Eq a, Semiring a, KnownNat n, 3 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiLaurent v n a
+pattern Z <- (isVar 2 -> True)
+  where Z = var 2
+
+var
+  :: forall v n a.
+     (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiLaurent v n a
+var i
+  | (one :: a) == zero = MultiLaurent 0 zero
+  | otherwise          = MultiLaurent
+      (SU.generate (\j -> if i == j then 1 else 0)) one
+{-# INLINE var #-}
+
+isVar
+  :: forall v n a.
+     (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiLaurent v n a
+  -> Bool
+isVar i (MultiLaurent off (MultiPoly xs))
+  | (one :: a) == zero
+  = off == 0 && G.null xs
+  | otherwise
+  = off == SU.generate (\j -> if i == j then 1 else 0)
+  && G.length xs == 1 && G.unsafeHead xs == (0, one)
+{-# INLINE isVar #-}
+
+-- | Used to construct monomials with negative powers.
+--
+-- This operator can be applied only to monomials with unit coefficients,
+-- but is instrumental to express Laurent polynomials
+-- in a mathematical fashion:
+--
+-- >>> X^-3 * Y^-1 :: UMultiLaurent 2 Int
+-- 1 * X^-3 * Y^-1
+-- >>> 3 * X^-1 + 2 * (Y^2)^-2 :: UMultiLaurent 2 Int
+-- 2 * Y^-4 + 3 * X^-1
+--
+-- @since 0.5.0.0
+(^-)
+  :: (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => MultiLaurent v n a
+  -> Int
+  -> MultiLaurent v n a
+MultiLaurent off (MultiPoly xs) ^- n
+  | G.length xs == 1, G.unsafeHead xs == (0, one)
+  = MultiLaurent (SU.map (* (-n)) off) (MultiPoly xs)
+  | otherwise
+  = throw $ PatternMatchFail "(^-) can be applied only to a monom with unit coefficient"
+
+instance {-# OVERLAPPING #-} (Eq a, Ring a, GcdDomain a, G.Vector v (SU.Vector 1 Word, a)) => GcdDomain (Laurent v a) where
+  divide (MultiLaurent off1 poly1) (MultiLaurent off2 poly2) =
+    toMultiLaurent (off1 - off2) <$> divide poly1 poly2
+  {-# INLINE divide #-}
+
+  gcd (MultiLaurent _ poly1) (MultiLaurent _ poly2) =
+    toMultiLaurent 0 (gcd poly1 poly2)
+  {-# INLINE gcd #-}
+
+  lcm (MultiLaurent _ poly1) (MultiLaurent _ poly2) =
+    toMultiLaurent 0 (lcm poly1 poly2)
+  {-# INLINE lcm #-}
+
+  coprime (MultiLaurent _ poly1) (MultiLaurent _ poly2) =
+    coprime poly1 poly2
+  {-# INLINE coprime #-}
+
+instance (Eq a, Ring a, GcdDomain a, KnownNat n, forall m. KnownNat m => G.Vector v (SU.Vector m Word, a), forall m. KnownNat m => Eq (v (SU.Vector m Word, a))) => GcdDomain (MultiLaurent v n a) where
+  divide (MultiLaurent off1 poly1) (MultiLaurent off2 poly2) =
+    toMultiLaurent (off1 - off2) <$> divide poly1 poly2
+  {-# INLINE divide #-}
+
+  gcd (MultiLaurent _ poly1) (MultiLaurent _ poly2) =
+    toMultiLaurent 0 (gcd poly1 poly2)
+  {-# INLINE gcd #-}
+
+  lcm (MultiLaurent _ poly1) (MultiLaurent _ poly2) =
+    toMultiLaurent 0 (lcm poly1 poly2)
+  {-# INLINE lcm #-}
+
+  coprime (MultiLaurent _ poly1) (MultiLaurent _ poly2) =
+    coprime poly1 poly2
+  {-# INLINE coprime #-}
+
+-------------------------------------------------------------------------------
+
+-- | Interpret a multivariate Laurent polynomial over 1+/m/ variables
+-- as a univariate Laurent polynomial, whose coefficients are
+-- multivariate Laurent polynomials over the last /m/ variables.
+--
+-- @since 0.5.0.0
+segregate
+  :: (KnownNat m, G.Vector v (SU.Vector (1 + m) Word, a), G.Vector v (SU.Vector m Word, a))
+  => MultiLaurent v (1 + m) a
+  -> VLaurent (MultiLaurent v m a)
+segregate (MultiLaurent off poly)
+  = toMultiLaurent (SU.take off)
+  $ MultiPoly
+  $ G.map (fmap (toMultiLaurent (SU.tail off)))
+  $ Multi.unMultiPoly
+  $ Multi.segregate poly
+
+-- | Interpret a univariate Laurent polynomials, whose coefficients are
+-- multivariate Laurent polynomials over the first /m/ variables,
+-- as a multivariate polynomial over 1+/m/ variables.
+--
+-- @since 0.5.0.0
+unsegregate
+  :: forall v m a.
+     (KnownNat m, KnownNat (1 + m), G.Vector v (SU.Vector (1 + m) Word, a), G.Vector v (SU.Vector m Word, a))
+  => VLaurent (MultiLaurent v m a)
+  -> MultiLaurent v (1 + m) a
+unsegregate (MultiLaurent off poly)
+  | G.null (unMultiPoly poly)
+  = MultiLaurent 0 (MultiPoly G.empty)
+  | otherwise
+  = toMultiLaurent (off SU.++ offs) (MultiPoly (G.concat (G.toList ys)))
+  where
+    xs :: V.Vector (SU.Vector 1 Word, (SU.Vector m Int, MultiPoly v m a))
+    xs = G.map (fmap unMultiLaurent) $ Multi.unMultiPoly poly
+    offs :: SU.Vector m Int
+    offs = G.foldl' (\acc (_, (v, _)) -> SU.zipWith min acc v) (SU.replicate maxBound) xs
+    ys :: V.Vector (v (SU.Vector (1 + m) Word, a))
+    ys = G.map (\(v, (vs, p)) -> G.map (first ((v SU.++) . SU.zipWith3 (\a b c -> c + fromIntegral (b - a)) offs vs)) (unMultiPoly p)) xs
diff --git a/src/Data/Poly/Internal/PolyOverField.hs b/src/Data/Poly/Internal/PolyOverField.hs
deleted file mode 100644
--- a/src/Data/Poly/Internal/PolyOverField.hs
+++ /dev/null
@@ -1,75 +0,0 @@
--- |
--- Module:      Data.Poly.Internal.PolyOverField
--- Copyright:   (c) 2019 Andrew Lelechenko
--- Licence:     BSD3
--- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
---
--- Wrapper with a more efficient 'Euclidean' instance.
---
-
-{-# LANGUAGE CPP                        #-}
-{-# LANGUAGE ConstraintKinds            #-}
-{-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE PatternSynonyms            #-}
-
-#if MIN_VERSION_semirings(0,4,2)
-
-module Data.Poly.Internal.PolyOverField
-  ( PolyOverField(..)
-  , PolyOverFractional
-  , pattern PolyOverFractional
-  , unPolyOverFractional
-  ) where
-
-import Prelude hiding (quotRem, quot, rem, gcd, lcm, (^))
-import Control.DeepSeq (NFData)
-import Data.Euclidean
-import Data.Semiring
-import qualified Data.Vector.Generic as G
-
-import qualified Data.Poly.Internal.Dense as Dense
-import qualified Data.Poly.Internal.Dense.Field as Dense (fieldGcd)
-
--- | Wrapper for polynomials over 'Field',
--- providing a faster 'GcdDomain' instance.
-newtype PolyOverField poly = PolyOverField { unPolyOverField :: poly }
-  deriving (Eq, NFData, Num, Ord, Ring, Semiring, Show)
-
--- |
-type PolyOverFractional = PolyOverField
-{-# DEPRECATED PolyOverFractional "Use 'PolyOverField'" #-}
-
--- |
-pattern PolyOverFractional :: poly -> PolyOverField poly
-pattern PolyOverFractional poly = PolyOverField poly
-
--- |
-unPolyOverFractional :: PolyOverField poly -> poly
-unPolyOverFractional = unPolyOverField
-{-# DEPRECATED unPolyOverFractional "Use 'unPolyOverField'" #-}
-
-#if !MIN_VERSION_semirings(0,5,0)
-type Field a = (Euclidean a, Ring a, Fractional a)
-#endif
-
-instance (Eq a, Eq (v a), Field a, G.Vector v a) => GcdDomain (PolyOverField (Dense.Poly v a)) where
-  gcd (PolyOverField x) (PolyOverField y) = PolyOverField (Dense.fieldGcd x y)
-  {-# INLINE gcd #-}
-
-instance (Eq a, Eq (v a), Field a, G.Vector v a) => Euclidean (PolyOverField (Dense.Poly v a)) where
-  degree (PolyOverField x) =
-    degree x
-  quotRem (PolyOverField x) (PolyOverField y) =
-    let (q, r) = quotRem x y in
-      (PolyOverField q, PolyOverField r)
-  {-# INLINE quotRem #-}
-  rem (PolyOverField x) (PolyOverField y) =
-    PolyOverField (rem x y)
-  {-# INLINE rem #-}
-
-#else
-
-module Data.Poly.Internal.PolyOverField () where
-
-#endif
diff --git a/src/Data/Poly/Internal/Sparse.hs b/src/Data/Poly/Internal/Sparse.hs
deleted file mode 100644
--- a/src/Data/Poly/Internal/Sparse.hs
+++ /dev/null
@@ -1,606 +0,0 @@
--- |
--- Module:      Data.Poly.Internal.Sparse
--- Copyright:   (c) 2019 Andrew Lelechenko
--- Licence:     BSD3
--- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
---
--- Sparse polynomials of one variable.
---
-
-{-# LANGUAGE CPP                        #-}
-{-# LANGUAGE FlexibleContexts           #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE PatternSynonyms            #-}
-{-# LANGUAGE ScopedTypeVariables        #-}
-{-# LANGUAGE StandaloneDeriving         #-}
-{-# LANGUAGE TypeFamilies               #-}
-{-# LANGUAGE UndecidableInstances       #-}
-{-# LANGUAGE ViewPatterns               #-}
-
-module Data.Poly.Internal.Sparse
-  ( Poly(..)
-  , VPoly
-  , UPoly
-  , leading
-  -- * Num interface
-  , toPoly
-  , monomial
-  , scale
-  , pattern X
-  , eval
-  , subst
-  , deriv
-  , integral
-  -- * Semiring interface
-  , toPoly'
-  , monomial'
-  , scale'
-  , pattern X'
-  , eval'
-  , subst'
-  , deriv'
-#if MIN_VERSION_semirings(0,5,0)
-  , integral'
-#endif
-  ) where
-
-import Prelude hiding (quot)
-import Control.DeepSeq (NFData)
-import Control.Monad
-import Control.Monad.Primitive
-import Control.Monad.ST
-import Data.Bits
-import Data.List (intersperse)
-import Data.Ord
-import Data.Semiring (Semiring(..), Ring())
-import qualified Data.Semiring as Semiring
-import qualified Data.Vector as V
-import qualified Data.Vector.Generic as G
-import qualified Data.Vector.Generic.Mutable as MG
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Algorithms.Tim as Tim
-import GHC.Exts
-#if !MIN_VERSION_semirings(0,4,0)
-import Data.Semigroup
-import Numeric.Natural
-#endif
-#if MIN_VERSION_semirings(0,5,0)
-import Data.Euclidean (Field, quot)
-#endif
-
--- | Polynomials of one variable with coefficients from @a@,
--- backed by a 'G.Vector' @v@ (boxed, unboxed, storable, etc.).
---
--- Use pattern 'X' for construction:
---
--- >>> (X + 1) + (X - 1) :: VPoly Integer
--- 2 * X
--- >>> (X + 1) * (X - 1) :: UPoly Int
--- 1 * X^2 + (-1)
---
--- Polynomials are stored normalized, without
--- zero coefficients, so 0 * 'X' + 1 equals to 1.
---
--- 'Ord' instance does not make much sense mathematically,
--- it is defined only for the sake of 'Data.Set.Set', 'Data.Map.Map', etc.
---
-newtype Poly v a = Poly
-  { unPoly :: v (Word, a)
-  -- ^ Convert 'Poly' to a vector of coefficients
-  -- (first element corresponds to a constant term).
-  }
-
-deriving instance Eq     (v (Word, a)) => Eq     (Poly v a)
-deriving instance Ord    (v (Word, a)) => Ord    (Poly v a)
-deriving instance NFData (v (Word, a)) => NFData (Poly v a)
-
-instance (Eq a, Semiring a, G.Vector v (Word, a)) => IsList (Poly v a) where
-  type Item (Poly v a) = (Word, a)
-  fromList = toPoly' . G.fromList
-  fromListN = (toPoly' .) . G.fromListN
-  toList = G.toList . unPoly
-
-instance (Show a, G.Vector v (Word, a)) => Show (Poly v a) where
-  showsPrec d (Poly xs)
-    | G.null xs
-      = showString "0"
-    | otherwise
-      = showParen (d > 0)
-      $ foldl (.) id
-      $ intersperse (showString " + ")
-      $ G.foldl (\acc (i, c) -> showCoeff i c : acc) [] xs
-    where
-      showCoeff 0 c = showsPrec 7 c
-      showCoeff 1 c = showsPrec 7 c . showString " * X"
-      showCoeff i c = showsPrec 7 c . showString " * X^" . showsPrec 7 i
-
--- | Polynomials backed by boxed vectors.
-type VPoly = Poly V.Vector
-
--- | Polynomials backed by unboxed vectors.
-type UPoly = Poly U.Vector
-
--- | Make 'Poly' from a list of (power, coefficient) pairs.
--- (first element corresponds to a constant term).
---
--- >>> :set -XOverloadedLists
--- >>> toPoly [(0,1),(1,2),(2,3)] :: VPoly Integer
--- 3 * X^2 + 2 * X + 1
--- >>> S.toPoly [(0,0),(1,0),(2,0)] :: UPoly Int
--- 0
-toPoly :: (Eq a, Num a, G.Vector v (Word, a)) => v (Word, a) -> Poly v a
-toPoly = Poly . normalize (/= 0) (+)
-
-toPoly' :: (Eq a, Semiring a, G.Vector v (Word, a)) => v (Word, a) -> Poly v a
-toPoly' = Poly . normalize (/= zero) plus
-
--- | Return a leading power and coefficient of a non-zero polynomial.
---
--- >>> leading ((2 * X + 1) * (2 * X^2 - 1) :: UPoly Int)
--- Just (3,4)
--- >>> leading (0 :: UPoly Int)
--- Nothing
-leading :: G.Vector v (Word, a) => Poly v a -> Maybe (Word, a)
-leading (Poly v)
-  | G.null v  = Nothing
-  | otherwise = Just (G.last v)
-
-normalize
-  :: G.Vector v (Word, a)
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> v (Word, a)
-  -> v (Word, a)
-normalize p add vs
-  | G.null vs = vs
-  | otherwise = runST $ do
-    ws <- G.thaw vs
-    l' <- normalizeM p add ws
-    G.unsafeFreeze $ MG.unsafeSlice 0 l' ws
-
-normalizeM
-  :: (PrimMonad m, G.Vector v (Word, a))
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> G.Mutable v (PrimState m) (Word, a)
-  -> m Int
-normalizeM p add ws = do
-    let l = MG.length ws
-    let go i j acc@(accP, accC)
-          | j >= l =
-            if p accC
-              then do
-                MG.write ws i acc
-                pure $ i + 1
-              else pure i
-          | otherwise = do
-            v@(vp, vc) <- MG.unsafeRead ws j
-            if vp == accP
-              then go i (j + 1) (accP, accC `add` vc)
-              else if p accC
-                then do
-                  MG.write ws i acc
-                  go (i + 1) (j + 1) v
-                else go i (j + 1) v
-    Tim.sortBy (comparing fst) ws
-    wsHead <- MG.unsafeRead ws 0
-    go 0 1 wsHead
-
--- | Note that 'abs' = 'id' and 'signum' = 'const' 1.
-instance (Eq a, Num a, G.Vector v (Word, a)) => Num (Poly v a) where
-  Poly xs + Poly ys = Poly $ plusPoly (/= 0) (+) xs ys
-  Poly xs - Poly ys = Poly $ minusPoly (/= 0) negate (-) xs ys
-  negate (Poly xs) = Poly $ G.map (fmap negate) xs
-  abs = id
-  signum = const 1
-  fromInteger n = case fromInteger n of
-    0 -> Poly G.empty
-    m -> Poly $ G.singleton (0, m)
-  Poly xs * Poly ys = Poly $ convolution (/= 0) (+) (*) xs ys
-  {-# INLINE (+) #-}
-  {-# INLINE (-) #-}
-  {-# INLINE negate #-}
-  {-# INLINE fromInteger #-}
-  {-# INLINE (*) #-}
-
-instance (Eq a, Semiring a, G.Vector v (Word, a)) => Semiring (Poly v a) where
-  zero = Poly G.empty
-  one
-    | (one :: a) == zero = zero
-    | otherwise = Poly $ G.singleton (0, one)
-  plus (Poly xs) (Poly ys) = Poly $ plusPoly (/= zero) plus xs ys
-  times (Poly xs) (Poly ys) = Poly $ convolution (/= zero) plus times xs ys
-  {-# INLINE zero #-}
-  {-# INLINE one #-}
-  {-# INLINE plus #-}
-  {-# INLINE times #-}
-
-#if MIN_VERSION_semirings(0,4,0)
-  fromNatural n = if n' == zero then zero else Poly $ G.singleton (0, n')
-    where
-      n' :: a
-      n' = fromNatural n
-  {-# INLINE fromNatural #-}
-#endif
-
-instance (Eq a, Ring a, G.Vector v (Word, a)) => Ring (Poly v a) where
-  negate (Poly xs) = Poly $ G.map (fmap Semiring.negate) xs
-
-plusPoly
-  :: G.Vector v (Word, a)
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> v (Word, a)
-  -> v (Word, a)
-  -> v (Word, a)
-plusPoly p add xs ys = runST $ do
-  zs <- MG.unsafeNew (G.length xs + G.length ys)
-  lenZs <- plusPolyM p add xs ys zs
-  G.unsafeFreeze $ MG.unsafeSlice 0 lenZs zs
-{-# INLINABLE plusPoly #-}
-
-plusPolyM
-  :: (PrimMonad m, G.Vector v (Word, a))
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> v (Word, a)
-  -> v (Word, a)
-  -> G.Mutable v (PrimState m) (Word, a)
-  -> m Int
-plusPolyM p add xs ys zs = go 0 0 0
-  where
-    lenXs = G.length xs
-    lenYs = G.length ys
-
-    go ix iy iz
-      | ix == lenXs, iy == lenYs = pure iz
-      | ix == lenXs = do
-        G.unsafeCopy
-          (MG.unsafeSlice iz (lenYs - iy) zs)
-          (G.unsafeSlice iy (lenYs - iy) ys)
-        pure $ iz + lenYs - iy
-      | iy == lenYs = do
-        G.unsafeCopy
-          (MG.unsafeSlice iz (lenXs - ix) zs)
-          (G.unsafeSlice ix (lenXs - ix) xs)
-        pure $ iz + lenXs - ix
-      | (xp, xc) <- G.unsafeIndex xs ix
-      , (yp, yc) <- G.unsafeIndex ys iy
-      = case xp `compare` yp of
-        LT -> do
-          MG.unsafeWrite zs iz (xp, xc)
-          go (ix + 1) iy (iz + 1)
-        EQ -> do
-          let zc = xc `add` yc
-          if p zc then do
-            MG.unsafeWrite zs iz (xp, zc)
-            go (ix + 1) (iy + 1) (iz + 1)
-          else
-            go (ix + 1) (iy + 1) iz
-        GT -> do
-          MG.unsafeWrite zs iz (yp, yc)
-          go ix (iy + 1) (iz + 1)
-{-# INLINABLE plusPolyM #-}
-
-minusPoly
-  :: G.Vector v (Word, a)
-  => (a -> Bool)
-  -> (a -> a)
-  -> (a -> a -> a)
-  -> v (Word, a)
-  -> v (Word, a)
-  -> v (Word, a)
-minusPoly p neg sub xs ys = runST $ do
-  zs <- MG.unsafeNew (lenXs + lenYs)
-  let go ix iy iz
-        | ix == lenXs, iy == lenYs = pure iz
-        | ix == lenXs = do
-          forM_ [iy .. lenYs - 1] $ \i ->
-            MG.unsafeWrite zs (iz + i - iy)
-              (fmap neg (G.unsafeIndex ys i))
-          pure $ iz + lenYs - iy
-        | iy == lenYs = do
-          G.unsafeCopy
-            (MG.unsafeSlice iz (lenXs - ix) zs)
-            (G.unsafeSlice ix (lenXs - ix) xs)
-          pure $ iz + lenXs - ix
-        | (xp, xc) <- G.unsafeIndex xs ix
-        , (yp, yc) <- G.unsafeIndex ys iy
-        = case xp `compare` yp of
-          LT -> do
-            MG.unsafeWrite zs iz (xp, xc)
-            go (ix + 1) iy (iz + 1)
-          EQ -> do
-            let zc = xc `sub` yc
-            if p zc then do
-              MG.unsafeWrite zs iz (xp, zc)
-              go (ix + 1) (iy + 1) (iz + 1)
-            else
-              go (ix + 1) (iy + 1) iz
-          GT -> do
-            MG.unsafeWrite zs iz (yp, neg yc)
-            go ix (iy + 1) (iz + 1)
-  lenZs <- go 0 0 0
-  G.unsafeFreeze $ MG.unsafeSlice 0 lenZs zs
-  where
-    lenXs = G.length xs
-    lenYs = G.length ys
-{-# INLINABLE minusPoly #-}
-
-scaleM
-  :: (PrimMonad m, G.Vector v (Word, a))
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> v (Word, a)
-  -> (Word, a)
-  -> G.Mutable v (PrimState m) (Word, a)
-  -> m Int
-scaleM p mul xs (yp, yc) zs = go 0 0
-  where
-    lenXs = G.length xs
-
-    go ix iz
-      | ix == lenXs = pure iz
-      | (xp, xc) <- G.unsafeIndex xs ix
-      = do
-        let zc = xc `mul` yc
-        if p zc then do
-          MG.unsafeWrite zs iz (xp + yp, zc)
-          go (ix + 1) (iz + 1)
-        else
-          go (ix + 1) iz
-{-# INLINABLE scaleM #-}
-
-scaleInternal
-  :: G.Vector v (Word, a)
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> Word
-  -> a
-  -> Poly v a
-  -> Poly v a
-scaleInternal p mul yp yc (Poly xs) = runST $ do
-  zs <- MG.unsafeNew (G.length xs)
-  len <- scaleM p (flip mul) xs (yp, yc) zs
-  fmap Poly $ G.unsafeFreeze $ MG.unsafeSlice 0 len zs
-{-# INLINABLE scaleInternal #-}
-
--- | Multiply a polynomial by a monomial, expressed as a power and a coefficient.
---
--- >>> scale 2 3 (X^2 + 1) :: UPoly Int
--- 3 * X^4 + 3 * X^2
-scale :: (Eq a, Num a, G.Vector v (Word, a)) => Word -> a -> Poly v a -> Poly v a
-scale = scaleInternal (/= 0) (*)
-
-scale' :: (Eq a, Semiring a, G.Vector v (Word, a)) => Word -> a -> Poly v a -> Poly v a
-scale' = scaleInternal (/= zero) times
-
-convolution
-  :: forall v a.
-     G.Vector v (Word, a)
-  => (a -> Bool)
-  -> (a -> a -> a)
-  -> (a -> a -> a)
-  -> v (Word, a)
-  -> v (Word, a)
-  -> v (Word, a)
-convolution p add mult xs ys
-  | G.length xs >= G.length ys
-  = go mult xs ys
-  | otherwise
-  = go (flip mult) ys xs
-  where
-    go :: (a -> a -> a) -> v (Word, a) -> v (Word, a) -> v (Word, a)
-    go mul long short = runST $ do
-      let lenLong   = G.length long
-          lenShort  = G.length short
-          lenBuffer = lenLong * lenShort
-      slices <- MG.unsafeNew lenShort
-      buffer <- MG.unsafeNew lenBuffer
-
-      forM_ [0 .. lenShort - 1] $ \iShort -> do
-        let (pShort, cShort) = G.unsafeIndex short iShort
-            from = iShort * lenLong
-            bufferSlice = MG.unsafeSlice from lenLong buffer
-        len <- scaleM p mul long (pShort, cShort) bufferSlice
-        MG.unsafeWrite slices iShort (from, len)
-
-      slices' <- G.unsafeFreeze slices
-      buffer' <- G.unsafeFreeze buffer
-      bufferNew <- MG.unsafeNew lenBuffer
-      gogo slices' buffer' bufferNew
-
-    gogo
-      :: PrimMonad m
-      => U.Vector (Int, Int)
-      -> v (Word, a)
-      -> G.Mutable v (PrimState m) (Word, a)
-      -> m (v (Word, a))
-    gogo slices buffer bufferNew
-      | G.length slices == 0
-      = pure G.empty
-      | G.length slices == 1
-      , (from, len) <- G.unsafeIndex slices 0
-      = pure $ G.unsafeSlice from len buffer
-      | otherwise = do
-        let nSlices = G.length slices
-        slicesNew <- MG.unsafeNew ((nSlices + 1) `shiftR` 1)
-        forM_ [0 .. (nSlices - 2) `shiftR` 1] $ \i -> do
-          let (from1, len1) = G.unsafeIndex slices (2 * i)
-              (from2, len2) = G.unsafeIndex slices (2 * i + 1)
-              slice1 = G.unsafeSlice from1 len1 buffer
-              slice2 = G.unsafeSlice from2 len2 buffer
-              slice3 = MG.unsafeSlice from1 (len1 + len2) bufferNew
-          len3 <- plusPolyM p add slice1 slice2 slice3
-          MG.unsafeWrite slicesNew i (from1, len3)
-
-        when (odd nSlices) $ do
-          let (from, len) = G.unsafeIndex slices (nSlices - 1)
-              slice1 = G.unsafeSlice from len buffer
-              slice3 = MG.unsafeSlice from len bufferNew
-          G.unsafeCopy slice3 slice1
-          MG.unsafeWrite slicesNew (nSlices `shiftR` 1) (from, len)
-
-        slicesNew' <- G.unsafeFreeze slicesNew
-        buffer'    <- G.unsafeThaw   buffer
-        bufferNew' <- G.unsafeFreeze bufferNew
-        gogo slicesNew' bufferNew' buffer'
-{-# INLINABLE convolution #-}
-
--- | Create a monomial from a power and a coefficient.
-monomial :: (Eq a, Num a, G.Vector v (Word, a)) => Word -> a -> Poly v a
-monomial _ 0 = Poly G.empty
-monomial p c = Poly $ G.singleton (p, c)
-
-monomial' :: (Eq a, Semiring a, G.Vector v (Word, a)) => Word -> a -> Poly v a
-monomial' p c
-  | c == zero = Poly G.empty
-  | otherwise = Poly $ G.singleton (p, c)
-
-data Strict3 a b c = Strict3 !a !b !c
-
-fst3 :: Strict3 a b c -> a
-fst3 (Strict3 a _ _) = a
-
--- | Evaluate at a given point.
---
--- >>> eval (X^2 + 1 :: UPoly Int) 3
--- 10
-eval :: (Num a, G.Vector v (Word, a)) => Poly v a -> a -> a
-eval = substitute (*)
-{-# INLINE eval #-}
-
-eval' :: (Semiring a, G.Vector v (Word, a)) => Poly v a -> a -> a
-eval' = substitute' times
-{-# INLINE eval' #-}
-
--- | Substitute another polynomial instead of 'X'.
---
--- >>> subst (X^2 + 1 :: UPoly Int) (X + 1 :: UPoly Int)
--- 1 * X^2 + 2 * X + 2
-subst
-  :: (Eq a, Num a, G.Vector v (Word, a), G.Vector w (Word, a))
-  => Poly v a
-  -> Poly w a
-  -> Poly w a
-subst = substitute (scale 0)
-{-# INLINE subst #-}
-
-subst'
-  :: (Eq a, Semiring a, G.Vector v (Word, a), G.Vector w (Word, a))
-  => Poly v a
-  -> Poly w a
-  -> Poly w a
-subst' = substitute' (scale' 0)
-{-# INLINE subst' #-}
-
-substitute :: (G.Vector v (Word, a), Num b) => (a -> b -> b) -> Poly v a -> b -> b
-substitute f (Poly cs) x = fst3 $ G.foldl' go (Strict3 0 0 1) cs
-  where
-    go (Strict3 acc q xq) (p, c) =
-      let xp = xq * x ^ (p - q) in
-        Strict3 (acc + f c xp) p xp
-{-# INLINE substitute #-}
-
-substitute' :: (G.Vector v (Word, a), Semiring b) => (a -> b -> b) -> Poly v a -> b -> b
-substitute' f (Poly cs) x = fst3 $ G.foldl' go (Strict3 zero 0 one) cs
-  where
-    go (Strict3 acc q xq) (p, c) =
-      let xp = xq `times` (if p == q then one else x Semiring.^ (p - q)) in
-        Strict3 (acc `plus` f c xp) p xp
-{-# INLINE substitute' #-}
-
--- | Take a derivative.
---
--- >>> deriv (X^3 + 3 * X) :: UPoly Int
--- 3 * X^2 + 3
-deriv :: (Eq a, Num a, G.Vector v (Word, a)) => Poly v a -> Poly v a
-deriv (Poly xs) = Poly $ derivPoly
-  (/= 0)
-  (\p c -> fromIntegral p * c)
-  xs
-{-# INLINE deriv #-}
-
-deriv' :: (Eq a, Semiring a, G.Vector v (Word, a)) => Poly v a -> Poly v a
-deriv' (Poly xs) = Poly $ derivPoly
-  (/= zero)
-  (\p c -> fromNatural (fromIntegral p) `times` c)
-  xs
-{-# INLINE deriv' #-}
-
-#if !MIN_VERSION_semirings(0,4,0)
-fromNatural :: Semiring a => Natural -> a
-fromNatural 0 = zero
-fromNatural n = getAdd' (stimes n (Add' one))
-
-newtype Add' a = Add' { getAdd' :: a }
-
-instance Semiring a => Semigroup (Add' a) where
-  Add' a <> Add' b = Add' (a `plus` b)
-#endif
-
-derivPoly
-  :: G.Vector v (Word, a)
-  => (a -> Bool)
-  -> (Word -> a -> a)
-  -> v (Word, a)
-  -> v (Word, a)
-derivPoly p mul xs
-  | G.null xs = G.empty
-  | otherwise = runST $ do
-    let lenXs = G.length xs
-    zs <- MG.unsafeNew lenXs
-    let go ix iz
-          | ix == lenXs = pure iz
-          | (xp, xc) <- G.unsafeIndex xs ix
-          = do
-            let zc = xp `mul` xc
-            if xp > 0 && p zc then do
-              MG.unsafeWrite zs iz (xp - 1, zc)
-              go (ix + 1) (iz + 1)
-            else
-              go (ix + 1) iz
-    lenZs <- go 0 0
-    G.unsafeFreeze $ MG.unsafeSlice 0 lenZs zs
-{-# INLINABLE derivPoly #-}
-
--- | Compute an indefinite integral of a polynomial,
--- setting constant term to zero.
---
--- >>> integral (3 * X^2 + 3) :: UPoly Double
--- 1.0 * X^3 + 3.0 * X
-integral :: (Eq a, Fractional a, G.Vector v (Word, a)) => Poly v a -> Poly v a
-integral (Poly xs)
-  = Poly
-  $ G.map (\(p, c) -> (p + 1, c / (fromIntegral p + 1))) xs
-{-# INLINE integral #-}
-
-#if MIN_VERSION_semirings(0,5,0)
-integral' :: (Eq a, Field a, G.Vector v (Word, a)) => Poly v a -> Poly v a
-integral' (Poly xs)
-  = Poly
-  $ G.map (\(p, c) -> (p + 1, c `quot` Semiring.fromIntegral (p + 1))) xs
-{-# INLINE integral' #-}
-#endif
-
--- | Create an identity polynomial.
-pattern X :: (Eq a, Num a, G.Vector v (Word, a), Eq (v (Word, a))) => Poly v a
-pattern X <- ((==) var -> True)
-  where X = var
-
-var :: forall a v. (Eq a, Num a, G.Vector v (Word, a), Eq (v (Word, a))) => Poly v a
-var
-  | (1 :: a) == 0 = Poly G.empty
-  | otherwise     = Poly $ G.singleton (1, 1)
-{-# INLINE var #-}
-
--- | Create an identity polynomial.
-pattern X' :: (Eq a, Semiring a, G.Vector v (Word, a), Eq (v (Word, a))) => Poly v a
-pattern X' <- ((==) var' -> True)
-  where X' = var'
-
-var' :: forall a v. (Eq a, Semiring a, G.Vector v (Word, a), Eq (v (Word, a))) => Poly v a
-var'
-  | (one :: a) == zero = Poly G.empty
-  | otherwise          = Poly $ G.singleton (1, one)
-{-# INLINE var' #-}
diff --git a/src/Data/Poly/Internal/Sparse/Field.hs b/src/Data/Poly/Internal/Sparse/Field.hs
deleted file mode 100644
--- a/src/Data/Poly/Internal/Sparse/Field.hs
+++ /dev/null
@@ -1,120 +0,0 @@
--- |
--- Module:      Data.Poly.Internal.Sparse.Field
--- Copyright:   (c) 2019 Andrew Lelechenko
--- Licence:     BSD3
--- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
---
--- GcdDomain for Field underlying
---
-
-{-# LANGUAGE ConstraintKinds            #-}
-{-# LANGUAGE CPP                        #-}
-{-# LANGUAGE FlexibleContexts           #-}
-{-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE PatternSynonyms            #-}
-{-# LANGUAGE ScopedTypeVariables        #-}
-{-# LANGUAGE TypeFamilies               #-}
-{-# LANGUAGE UndecidableInstances       #-}
-
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
-#if MIN_VERSION_semirings(0,4,2)
-
-module Data.Poly.Internal.Sparse.Field
-  ( gcdExt
-  ) where
-
-import Prelude hiding (quotRem, quot, rem, gcd)
-import Control.Arrow
-import Control.Exception
-import Data.Euclidean (Euclidean(..))
-#if !MIN_VERSION_semirings(0,5,0)
-import Data.Semiring (Ring)
-#else
-import Data.Euclidean (Field)
-#endif
-import Data.Semiring (minus, plus, times, zero, one)
-import qualified Data.Vector.Generic as G
-
-import Data.Poly.Internal.Sparse
-import Data.Poly.Internal.Sparse.GcdDomain ()
-
-#if !MIN_VERSION_semirings(0,5,0)
-type Field a = (Euclidean a, Ring a, Fractional a)
-#endif
-
-instance (Eq a, Eq (v (Word, a)), Field a, G.Vector v (Word, a)) => Euclidean (Poly v a) where
-  degree (Poly xs)
-    | G.null xs = 0
-    | otherwise = 1 + fromIntegral (fst (G.last xs))
-
-  quotRem = quotientRemainder
-
-quotientRemainder
-  :: (Eq a, Field a, G.Vector v (Word, a))
-  => Poly v a
-  -> Poly v a
-  -> (Poly v a, Poly v a)
-quotientRemainder ts ys = case leading ys of
-  Nothing -> throw DivideByZero
-  Just (yp, yc) -> go ts
-    where
-      go xs = case leading xs of
-        Nothing -> (zero, zero)
-        Just (xp, xc) -> case xp `compare` yp of
-          LT -> (zero, xs)
-          EQ -> (zs, xs')
-          GT -> first (`plus` zs) $ go xs'
-          where
-            zs = Poly $ G.singleton (xp `minus` yp, xc `quot` yc)
-            xs' = xs `minus` zs `times` ys
-
--- | Execute the extended Euclidean algorithm.
--- For polynomials @a@ and @b@, compute their unique greatest common divisor @g@
--- and the unique coefficient polynomial @s@ satisfying @as + bt = g@,
--- such that either @g@ is monic, or @g = 0@ and @s@ is monic, or @g = s = 0@.
---
--- >>> gcdExt (X^2 + 1 :: UPoly Double) (X^3 + 3 * X :: UPoly Double)
--- (1.0, 0.5 * X^2 + (-0.0) * X + 1.0)
--- >>> gcdExt (X^3 + 3 * X :: UPoly Double) (3 * X^4 + 3 * X^2 :: UPoly Double)
--- (1.0 * X + 0.0,(-0.16666666666666666) * X^2 + (-0.0) * X + 0.3333333333333333)
-gcdExt
-  :: (Eq a, Field a, G.Vector v (Word, a), Eq (v (Word, a)))
-  => Poly v a
-  -> Poly v a
-  -> (Poly v a, Poly v a)
-gcdExt xs ys = case scaleMonic gs of
-  Just (c', gs') -> (gs', scale' zero c' ss)
-  Nothing -> case scaleMonic ss of
-    Just (_, ss') -> (zero, ss')
-    Nothing -> (zero, zero)
-  where
-    (gs, ss) = go ys xs zero one
-      where
-        go r' r s' s
-          | r' == zero = (r, s)
-          | otherwise  = case r `quotRem` r' of
-            (q, r'') -> go r'' r' (s `minus` q `times` s') s'
-{-# INLINABLE gcdExt #-}
-
--- | Scale a non-zero polynomial such that its leading coefficient is one,
--- returning the reciprocal of the leading coefficient in the scaling.
---
--- >>> scaleMonic (X^3 + 3 * X :: UPoly Double)
--- Just (1.0, 1.0 * X^3 + 0.0 * X^2 + 3.0 * X + 0.0)
--- >>> scaleMonic (3 * X^4 + 3 * X^2 :: UPoly Double)
--- Just (0.3333333333333333, 1.0 * X^4 + 0.0 * X^3 + 1.0 * X^2 + 0.0 * X + 0.0)
-scaleMonic
-  :: (Eq a, Field a, G.Vector v (Word, a), Eq (v (Word, a)))
-  => Poly v a
-  -> Maybe (a, Poly v a)
-scaleMonic xs = case leading xs of
-  Nothing -> Nothing
-  Just (_, c) -> let c' = one `quot` c in Just (c', scale' zero c' xs)
-{-# INLINE scaleMonic #-}
-
-#else
-
-module Data.Poly.Internal.Sparse.Field () where
-
-#endif
diff --git a/src/Data/Poly/Internal/Sparse/GcdDomain.hs b/src/Data/Poly/Internal/Sparse/GcdDomain.hs
deleted file mode 100644
--- a/src/Data/Poly/Internal/Sparse/GcdDomain.hs
+++ /dev/null
@@ -1,79 +0,0 @@
--- |
--- Module:      Data.Poly.Internal.Sparse.GcdDomain
--- Copyright:   (c) 2019 Andrew Lelechenko
--- Licence:     BSD3
--- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
---
--- GcdDomain for GcdDomain underlying
---
-
-{-# LANGUAGE CPP                        #-}
-{-# LANGUAGE FlexibleContexts           #-}
-{-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE PatternSynonyms            #-}
-{-# LANGUAGE ScopedTypeVariables        #-}
-{-# LANGUAGE TypeFamilies               #-}
-{-# LANGUAGE UndecidableInstances       #-}
-
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
-module Data.Poly.Internal.Sparse.GcdDomain
-  () where
-
-#if MIN_VERSION_semirings(0,4,2)
-
-import Prelude hiding (gcd, lcm, (^))
-import Control.Exception
-import Data.Euclidean
-import Data.Maybe
-import Data.Semiring (Semiring(..), Ring(), minus)
-import qualified Data.Vector.Generic as G
-
-import Data.Poly.Internal.Sparse
-
--- | Consider using 'Data.Poly.Sparse.Semiring.PolyOverField' wrapper,
--- which provides a much faster implementation of
--- 'Data.Euclidean.gcd' for polynomials over 'Field'.
-instance (Eq a, Ring a, GcdDomain a, Eq (v (Word, a)), G.Vector v (Word, a)) => GcdDomain (Poly v a) where
-  divide xs ys = case leading ys of
-    Nothing -> throw DivideByZero
-    Just (yp, yc) -> case leading xs of
-      Nothing -> Just xs
-      Just (xp, xc)
-        | xp < yp -> Nothing
-        | otherwise -> do
-          zc <- divide xc yc
-          let z = Poly $ G.singleton (xp - yp, zc)
-          rest <- divide (xs `minus` z `times` ys) ys
-          pure $ rest `plus` z
-
-  gcd xs ys
-    | G.null (unPoly xs) = ys
-    | G.null (unPoly ys) = xs
-    | otherwise = maybe err (times xy) (divide zs (monomial' 0 (cont zs)))
-      where
-        err = error "gcd: violated internal invariant"
-        zs = gcdHelper xs ys
-        cont ts = G.foldl' (\acc (_, t) -> gcd acc t) zero (unPoly ts)
-        xy = monomial' 0 (gcd (cont xs) (cont ys))
-
-gcdHelper
-  :: (Eq a, Ring a, GcdDomain a, G.Vector v (Word, a))
-  => Poly v a
-  -> Poly v a
-  -> Poly v a
-gcdHelper xs ys = case leading xs of
-  Nothing -> ys
-  Just (xp, xc) -> case leading ys of
-    Nothing -> xs
-    Just (yp, yc) -> case xp `compare` yp of
-      LT -> gcdHelper xs (ys `times` monomial' 0 gy `minus` xs `times` monomial' (yp - xp) gx)
-      EQ -> gcdHelper xs (ys `times` monomial' 0 gy `minus` xs `times` monomial' 0 gx)
-      GT -> gcdHelper (xs `times` monomial' 0 gx `minus` ys `times` monomial' (xp - yp) gy) ys
-      where
-        g = lcm xc yc
-        gx = fromMaybe err $ divide g xc
-        gy = fromMaybe err $ divide g yc
-        err = error "gcd: violated internal invariant"
-
-#endif
diff --git a/src/Data/Poly/Laurent.hs b/src/Data/Poly/Laurent.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Laurent.hs
@@ -0,0 +1,30 @@
+-- |
+-- Module:      Data.Poly.Laurent
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>.
+--
+-- @since 0.4.0.0
+--
+
+{-# LANGUAGE PatternSynonyms            #-}
+
+module Data.Poly.Laurent
+  ( Laurent
+  , VLaurent
+  , ULaurent
+  , unLaurent
+  , toLaurent
+  , leading
+  , monomial
+  , scale
+  , pattern X
+  , (^-)
+  , eval
+  , subst
+  , deriv
+  ) where
+
+import Data.Poly.Internal.Dense.Laurent
diff --git a/src/Data/Poly/Multi.hs b/src/Data/Poly/Multi.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Multi.hs
@@ -0,0 +1,36 @@
+-- |
+-- Module:      Data.Poly.Multi
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Sparse multivariate polynomials with 'Num' instance.
+--
+-- @since 0.5.0.0
+
+{-# LANGUAGE DataKinds        #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE PatternSynonyms  #-}
+
+module Data.Poly.Multi
+  ( MultiPoly
+  , VMultiPoly
+  , UMultiPoly
+  , unMultiPoly
+  , toMultiPoly
+  , monomial
+  , scale
+  , pattern X
+  , pattern Y
+  , pattern Z
+  , eval
+  , subst
+  , deriv
+  , integral
+  , segregate
+  , unsegregate
+  ) where
+
+import Data.Poly.Internal.Multi
+import Data.Poly.Internal.Multi.Field ()
+import Data.Poly.Internal.Multi.GcdDomain ()
diff --git a/src/Data/Poly/Multi/Laurent.hs b/src/Data/Poly/Multi/Laurent.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Multi/Laurent.hs
@@ -0,0 +1,32 @@
+-- |
+-- Module:      Data.Poly.Multi.Laurent
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Sparse multivariate
+-- <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>.
+--
+
+{-# LANGUAGE PatternSynonyms            #-}
+
+module Data.Poly.Multi.Laurent
+  ( MultiLaurent
+  , VMultiLaurent
+  , UMultiLaurent
+  , unMultiLaurent
+  , toMultiLaurent
+  , monomial
+  , scale
+  , pattern X
+  , pattern Y
+  , pattern Z
+  , (^-)
+  , eval
+  , subst
+  , deriv
+  , segregate
+  , unsegregate
+  ) where
+
+import Data.Poly.Internal.Multi.Laurent
diff --git a/src/Data/Poly/Multi/Semiring.hs b/src/Data/Poly/Multi/Semiring.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Multi/Semiring.hs
@@ -0,0 +1,178 @@
+-- |
+-- Module:      Data.Poly.Multi.Semiring
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Sparse multivariate polynomials with a 'Semiring' instance.
+--
+-- @since 0.5.0.0
+
+{-# LANGUAGE DataKinds        #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE PatternSynonyms  #-}
+{-# LANGUAGE TypeFamilies     #-}
+{-# LANGUAGE TypeOperators    #-}
+
+module Data.Poly.Multi.Semiring
+  ( MultiPoly
+  , VMultiPoly
+  , UMultiPoly
+  , unMultiPoly
+  , toMultiPoly
+  , monomial
+  , scale
+  , pattern X
+  , pattern Y
+  , pattern Z
+  , eval
+  , subst
+  , deriv
+  , integral
+  , segregate
+  , unsegregate
+  ) where
+
+import Data.Finite
+import Data.Euclidean (Field)
+import Data.Semiring (Semiring(..))
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Sized as SG
+import qualified Data.Vector.Sized as SV
+import qualified Data.Vector.Unboxed.Sized as SU
+import GHC.TypeNats (KnownNat, type (<=))
+
+import Data.Poly.Internal.Multi (MultiPoly, VMultiPoly, UMultiPoly, unMultiPoly, segregate, unsegregate)
+import qualified Data.Poly.Internal.Multi as Multi
+import Data.Poly.Internal.Multi.Field ()
+import Data.Poly.Internal.Multi.GcdDomain ()
+
+-- | Make a 'MultiPoly' from a list of (powers, coefficient) pairs.
+--
+-- >>> :set -XOverloadedLists -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> toMultiPoly [(fromTuple (0,0),1),(fromTuple (0,1),2),(fromTuple (1,0),3)] :: VMultiPoly 2 Integer
+-- 3 * X + 2 * Y + 1
+-- >>> toMultiPoly [(fromTuple (0,0),0),(fromTuple (0,1),0),(fromTuple (1,0),0)] :: UMultiPoly 2 Int
+-- 0
+--
+-- @since 0.5.0.0
+toMultiPoly
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a))
+  => v (SU.Vector n Word, a)
+  -> MultiPoly v n a
+toMultiPoly = Multi.toMultiPoly'
+
+-- | Create a monomial from powers and a coefficient.
+--
+-- @since 0.5.0.0
+monomial
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Word
+  -> a
+  -> MultiPoly v n a
+monomial = Multi.monomial'
+
+-- | Multiply a polynomial by a monomial, expressed as powers and a coefficient.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> scale (fromTuple (1, 1)) 3 (X^2 + Y) :: UMultiPoly 2 Int
+-- 3 * X^3 * Y + 3 * X * Y^2
+--
+-- @since 0.5.0.0
+scale
+  :: (Eq a, Semiring a, KnownNat n, G.Vector v (SU.Vector n Word, a))
+  => SU.Vector n Word
+  -> a
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+scale = Multi.scale'
+
+-- | Create a polynomial equal to the first variable.
+--
+-- @since 0.5.0.0
+pattern X
+  :: (Eq a, Semiring a, KnownNat n, 1 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern X = Multi.X'
+
+-- | Create a polynomial equal to the second variable.
+--
+-- @since 0.5.0.0
+pattern Y
+  :: (Eq a, Semiring a, KnownNat n, 2 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern Y = Multi.Y'
+
+-- | Create a polynomial equal to the third variable.
+--
+-- @since 0.5.0.0
+pattern Z
+  :: (Eq a, Semiring a, KnownNat n, 3 <= n, G.Vector v (SU.Vector n Word, a))
+  => MultiPoly v n a
+pattern Z = Multi.Z'
+
+-- | Evaluate the polynomial at a given point.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> eval (X^2 + Y^2 :: UMultiPoly 2 Int) (fromTuple (3, 4) :: Data.Vector.Sized.Vector 2 Int)
+-- 25
+--
+-- @since 0.5.0.0
+eval
+  :: (Semiring a, G.Vector v (SU.Vector n Word, a), G.Vector u a)
+  => MultiPoly v n a
+  -> SG.Vector u n a
+  -> a
+eval = Multi.eval'
+
+-- | Substitute other polynomials instead of the variables.
+--
+-- >>> :set -XDataKinds
+-- >>> import Data.Vector.Generic.Sized (fromTuple)
+-- >>> subst (X^2 + Y^2 + Z^2 :: UMultiPoly 3 Int) (fromTuple (X + 1, Y + 1, X + Y :: UMultiPoly 2 Int))
+-- 2 * X^2 + 2 * X * Y + 2 * X + 2 * Y^2 + 2 * Y + 2
+--
+-- @since 0.5.0.0
+subst
+  :: (Eq a, Semiring a, KnownNat m, G.Vector v (SU.Vector n Word, a), G.Vector w (SU.Vector m Word, a))
+  => MultiPoly v n a
+  -> SV.Vector n (MultiPoly w m a)
+  -> MultiPoly w m a
+subst = Multi.subst'
+
+-- | Take the derivative of the polynomial with respect to the /i/-th variable.
+--
+-- >>> :set -XDataKinds
+-- >>> deriv 0 (X^3 + 3 * Y) :: UMultiPoly 2 Int
+-- 3 * X^2
+-- >>> deriv 1 (X^3 + 3 * Y) :: UMultiPoly 2 Int
+-- 3
+--
+-- @since 0.5.0.0
+deriv
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+deriv = Multi.deriv'
+
+-- | Compute an indefinite integral of the polynomial
+-- with respect to the /i/-th variable,
+-- setting the constant term to zero.
+--
+-- >>> :set -XDataKinds
+-- >>> integral 0 (3 * X^2 + 2 * Y) :: UMultiPoly 2 Double
+-- 1.0 * X^3 + 2.0 * X * Y
+-- >>> integral 1 (3 * X^2 + 2 * Y) :: UMultiPoly 2 Double
+-- 3.0 * X^2 * Y + 1.0 * Y^2
+--
+-- @since 0.5.0.0
+integral
+  :: (Field a, G.Vector v (SU.Vector n Word, a))
+  => Finite n
+  -> MultiPoly v n a
+  -> MultiPoly v n a
+integral = Multi.integral'
diff --git a/src/Data/Poly/Orthogonal.hs b/src/Data/Poly/Orthogonal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Orthogonal.hs
@@ -0,0 +1,149 @@
+-- |
+-- Module:      Data.Poly.Orthogonal
+-- Copyright:   (c) 2019 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Classical orthogonal polynomials.
+--
+-- @since 0.4.0.0
+
+{-# LANGUAGE OverloadedLists     #-}
+{-# LANGUAGE RebindableSyntax    #-}
+
+module Data.Poly.Orthogonal
+  ( legendre
+  , legendreShifted
+  , gegenbauer
+  , jacobi
+  , chebyshev1
+  , chebyshev2
+  , hermiteProb
+  , hermitePhys
+  , laguerre
+  , laguerreGen
+  ) where
+
+import Prelude hiding (quot, Num(..), fromIntegral)
+import Data.Euclidean
+import Data.Semiring
+import Data.Poly.Semiring
+import Data.Poly.Internal.Dense (unscale')
+import Data.Vector.Generic (Vector, fromListN)
+
+-- | <https://en.wikipedia.org/wiki/Legendre_polynomials Legendre polynomials>.
+--
+-- >>> take 3 legendre :: [Data.Poly.VPoly Double]
+-- [1.0,1.0 * X + 0.0,1.5 * X^2 + 0.0 * X + (-0.5)]
+--
+-- @since 0.4.0.0
+legendre :: (Eq a, Field a, Vector v a) => [Poly v a]
+legendre = map (`subst'` toPoly [1 `quot` 2, 1 `quot` 2]) legendreShifted
+  where
+    subst' :: (Eq a, Semiring a, Vector v a) => Poly v a -> Poly v a -> Poly v a
+    subst' = subst
+
+-- | <https://en.wikipedia.org/wiki/Legendre_polynomials#Shifted_Legendre_polynomials Shifted Legendre polynomials>.
+--
+-- >>> take 3 legendreShifted :: [Data.Poly.VPoly Integer]
+-- [1,2 * X + (-1),6 * X^2 + (-6) * X + 1]
+--
+-- @since 0.4.0.0
+legendreShifted :: (Eq a, Euclidean a, Ring a, Vector v a) => [Poly v a]
+legendreShifted = xs
+  where
+    xs = 1 : toPoly [-1, 2] : zipWith3 rec (iterate (+ 1) 1) xs (tail xs)
+    rec n pm1 p = unscale' 0 (n + 1) (toPoly [-1 - 2 * n, 2 + 4 * n] * p - scale 0 n pm1)
+
+-- | <https://en.wikipedia.org/wiki/Gegenbauer_polynomials Gegenbauer polynomials>.
+--
+-- @since 0.4.0.0
+gegenbauer :: (Eq a, Field a, Vector v a) => a -> [Poly v a]
+gegenbauer g = jacobi a a
+  where
+    a = g - 1 `quot` 2
+
+-- | <https://en.wikipedia.org/wiki/Jacobi_polynomials Jacobi polynomials>.
+--
+-- @since 0.4.0.0
+jacobi :: (Eq a, Field a, Vector v a) => a -> a -> [Poly v a]
+jacobi a b = xs
+  where
+    x0 = 1
+    x1 = toPoly [(a - b) `quot` 2, (a + b + 2) `quot` 2]
+    xs = x0 : x1 : zipWith3 rec (iterate (+ 1) 2) xs (tail xs)
+    rec n pm1 p = toPoly [d, c] * p - scale 0 cm1 pm1
+      where
+        cp1 = 2 * n * (n + a + b) * (2 * n + a + b - 2)
+        q   = (2 * n + a + b - 1) `quot` cp1
+        c   = q * ((2 * n + a + b) * (2 * n + a + b - 2))
+        d   = q * (a * a - b * b)
+        cm1 = 2 * (n + a - 1) * (n + b - 1) * (2 * n + a + b) `quot` cp1
+
+-- | <https://en.wikipedia.org/wiki/Chebyshev_polynomials Chebyshev polynomials>
+-- of the first kind.
+--
+-- >>> take 3 chebyshev1 :: [Data.Poly.VPoly Integer]
+-- [1,1 * X + 0,2 * X^2 + 0 * X + (-1)]
+--
+-- @since 0.4.0.0
+chebyshev1 :: (Eq a, Ring a, Vector v a) => [Poly v a]
+chebyshev1 = xs
+  where
+    xs = 1 : monomial 1 1 : zipWith (\pm1 p -> scale 1 2 p - pm1) xs (tail xs)
+
+-- | <https://en.wikipedia.org/wiki/Chebyshev_polynomials Chebyshev polynomials>
+-- of the second kind.
+--
+-- >>> take 3 chebyshev2 :: [Data.Poly.VPoly Integer]
+-- [1,2 * X + 0,4 * X^2 + 0 * X + (-1)]
+--
+-- @since 0.4.0.0
+chebyshev2 :: (Eq a, Ring a, Vector v a) => [Poly v a]
+chebyshev2 = xs
+  where
+    xs = 1 : monomial 1 2 : zipWith (\pm1 p -> scale 1 2 p - pm1) xs (tail xs)
+
+-- | Probabilists' <https://en.wikipedia.org/wiki/Hermite_polynomials Hermite polynomials>.
+--
+-- >>> take 3 hermiteProb :: [Data.Poly.VPoly Integer]
+-- [1,1 * X + 0,1 * X^2 + 0 * X + (-1)]
+--
+-- @since 0.4.0.0
+hermiteProb :: (Eq a, Ring a, Vector v a) => [Poly v a]
+hermiteProb = xs
+  where
+    xs = 1 : monomial 1 1 : zipWith3 rec (iterate (+ 1) 1) xs (tail xs)
+    rec n pm1 p = scale 1 1 p - scale 0 n pm1
+
+-- | Physicists' <https://en.wikipedia.org/wiki/Hermite_polynomials Hermite polynomials>.
+--
+-- >>> take 3 hermitePhys :: [Data.Poly.VPoly Double]
+-- [1.0,2.0 * X + 0.0,4.0 * X^2 + 0.0 * X + (-2.0)]
+--
+-- @since 0.4.0.0
+hermitePhys :: (Eq a, Ring a, Vector v a) => [Poly v a]
+hermitePhys = xs
+  where
+    xs = 1 : monomial 1 2 : zipWith3 rec (iterate (+ 1) 1) xs (tail xs)
+    rec n pm1 p = scale 1 2 p - scale 0 (2 * n) pm1
+
+-- | <https://en.wikipedia.org/wiki/Laguerre_polynomials Laguerre polynomials>.
+--
+-- >>> take 3 laguerre :: [Data.Poly.VPoly Double]
+-- [1.0,(-1.0) * X + 1.0,0.5 * X^2 + (-2.0) * X + 1.0]
+--
+-- @since 0.4.0.0
+laguerre :: (Eq a, Field a, Vector v a) => [Poly v a]
+laguerre = laguerreGen 0
+
+-- | <https://en.wikipedia.org/wiki/Laguerre_polynomials#Generalized_Laguerre_polynomials Generalized Laguerre polynomials>
+--
+-- @since 0.4.0.0
+laguerreGen :: (Eq a, Field a, Vector v a) => a -> [Poly v a]
+laguerreGen a = xs
+  where
+    x0 = 1
+    x1 = toPoly [1 + a, -1]
+    xs = x0 : x1 : zipWith3 rec (iterate (+ 1) 1) xs (tail xs)
+    rec n pm1 p = toPoly [(2 * n + 1 + a) `quot` (n + 1), -1 `quot` (n + 1)] * p - scale 0 ((n + a) `quot` (n + 1)) pm1
diff --git a/src/Data/Poly/Semiring.hs b/src/Data/Poly/Semiring.hs
--- a/src/Data/Poly/Semiring.hs
+++ b/src/Data/Poly/Semiring.hs
@@ -6,9 +6,12 @@
 --
 -- Dense polynomials and a 'Semiring'-based interface.
 --
+-- @since 0.2.0.0
 
-{-# LANGUAGE CPP             #-}
-{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE CPP              #-}
+{-# LANGUAGE DataKinds        #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE PatternSynonyms  #-}
 
 module Data.Poly.Semiring
   ( Poly
@@ -16,7 +19,6 @@
   , UPoly
   , unPoly
   , leading
-  -- * Semiring interface
   , toPoly
   , monomial
   , scale
@@ -24,45 +26,50 @@
   , eval
   , subst
   , deriv
-#if MIN_VERSION_semirings(0,5,0)
   , integral
-#endif
-#if MIN_VERSION_semirings(0,4,2)
-  -- * Polynomials over 'Field'
-  , PolyOverField(..)
-  , gcdExt
-  , PolyOverFractional
-  , pattern PolyOverFractional
-  , unPolyOverFractional
+  , timesRing
+#ifdef SupportSparse
+  , denseToSparse
+  , sparseToDense
 #endif
+  , dft
+  , inverseDft
+  , dftMult
   ) where
 
-import Data.Semiring (Semiring)
+import Data.Bits
+import Data.Euclidean (Field)
+import Data.Semiring (Semiring(..))
 import qualified Data.Vector.Generic as G
 
-import Data.Poly.Internal.Dense (Poly(..), VPoly, UPoly, leading)
+import Data.Poly.Internal.Dense (Poly(..), VPoly, UPoly, leading, timesRing)
 import qualified Data.Poly.Internal.Dense as Dense
-#if MIN_VERSION_semirings(0,4,2)
-import Data.Poly.Internal.Dense.Field (gcdExt)
+import Data.Poly.Internal.Dense.Field ()
+import Data.Poly.Internal.Dense.DFT
 import Data.Poly.Internal.Dense.GcdDomain ()
-import Data.Poly.Internal.PolyOverField
-#endif
-#if MIN_VERSION_semirings(0,5,0)
-import Data.Euclidean (Field)
+
+#ifdef SupportSparse
+import qualified Data.Vector.Unboxed.Sized as SU
+import qualified Data.Poly.Internal.Multi as Sparse
+import qualified Data.Poly.Internal.Convert as Convert
 #endif
 
--- | Make 'Poly' from a vector of coefficients
--- (first element corresponds to a constant term).
+-- | Make a 'Poly' from a vector of coefficients
+-- (first element corresponds to the constant term).
 --
 -- >>> :set -XOverloadedLists
 -- >>> toPoly [1,2,3] :: VPoly Integer
 -- 3 * X^2 + 2 * X + 1
 -- >>> toPoly [0,0,0] :: UPoly Int
 -- 0
+--
+-- @since 0.2.0.0
 toPoly :: (Eq a, Semiring a, G.Vector v a) => v a -> Poly v a
 toPoly = Dense.toPoly'
 
 -- | Create a monomial from a power and a coefficient.
+--
+-- @since 0.3.0.0
 monomial :: (Eq a, Semiring a, G.Vector v a) => Word -> a -> Poly v a
 monomial = Dense.monomial'
 
@@ -70,17 +77,25 @@
 --
 -- >>> scale 2 3 (X^2 + 1) :: UPoly Int
 -- 3 * X^4 + 0 * X^3 + 3 * X^2 + 0 * X + 0
+--
+-- @since 0.3.0.0
 scale :: (Eq a, Semiring a, G.Vector v a) => Word -> a -> Poly v a -> Poly v a
 scale = Dense.scale'
 
--- | Create an identity polynomial.
-pattern X :: (Eq a, Semiring a, G.Vector v a, Eq (v a)) => Poly v a
+-- | The polynomial 'X'.
+--
+-- > X == monomial 1 one
+--
+-- @since 0.2.0.0
+pattern X :: (Eq a, Semiring a, G.Vector v a) => Poly v a
 pattern X = Dense.X'
 
--- | Evaluate at a given point.
+-- | Evaluate the polynomial at a given point.
 --
 -- >>> eval (X^2 + 1 :: UPoly Int) 3
 -- 10
+--
+-- @since 0.2.0.0
 eval :: (Semiring a, G.Vector v a) => Poly v a -> a -> a
 eval = Dense.eval'
 
@@ -88,22 +103,76 @@
 --
 -- >>> subst (X^2 + 1 :: UPoly Int) (X + 1 :: UPoly Int)
 -- 1 * X^2 + 2 * X + 2
+--
+-- @since 0.3.3.0
 subst :: (Eq a, Semiring a, G.Vector v a, G.Vector w a) => Poly v a -> Poly w a -> Poly w a
 subst = Dense.subst'
 
--- | Take a derivative.
+-- | Take the derivative of the polynomial.
 --
 -- >>> deriv (X^3 + 3 * X) :: UPoly Int
 -- 3 * X^2 + 0 * X + 3
+--
+-- @since 0.2.0.0
 deriv :: (Eq a, Semiring a, G.Vector v a) => Poly v a -> Poly v a
 deriv = Dense.deriv'
 
-#if MIN_VERSION_semirings(0,5,0)
--- | Compute an indefinite integral of a polynomial,
--- setting constant term to zero.
+-- | Compute an indefinite integral of the polynomial,
+-- setting the constant term to zero.
 --
 -- >>> integral (3 * X^2 + 3) :: UPoly Double
 -- 1.0 * X^3 + 0.0 * X^2 + 3.0 * X + 0.0
+--
+-- @since 0.3.2.0
 integral :: (Eq a, Field a, G.Vector v a) => Poly v a -> Poly v a
 integral = Dense.integral'
+
+-- | Multiplication of polynomials using
+-- <https://en.wikipedia.org/wiki/Fast_Fourier_transform discrete Fourier transform>.
+-- It could be faster than '(*)' for large polynomials
+-- if multiplication of coefficients is particularly expensive.
+--
+-- @since 0.5.0.0
+dftMult
+  :: (Eq a, Field a, G.Vector v a)
+  => (Int -> a) -- ^ mapping from \( N = 2^n \) to a primitive root \( \sqrt[N]{1} \)
+  -> Poly v a
+  -> Poly v a
+  -> Poly v a
+dftMult getPrimRoot (Poly xs) (Poly ys) =
+  toPoly $ inverseDft primRoot $ G.zipWith times (dft primRoot xs') (dft primRoot ys')
+  where
+    nextPowerOf2 :: Int -> Int
+    nextPowerOf2 0 = 1
+    nextPowerOf2 1 = 1
+    nextPowerOf2 x = 1 `unsafeShiftL` (finiteBitSize (0 :: Int) - countLeadingZeros (x - 1))
+
+    padTo l vs = G.generate l (\k -> if k < G.length vs then vs G.! k else zero)
+
+    zl = nextPowerOf2 (G.length xs + G.length ys)
+    xs' = padTo zl xs
+    ys' = padTo zl ys
+    primRoot = getPrimRoot zl
+{-# INLINABLE dftMult #-}
+
+#ifdef SupportSparse
+-- | Convert from dense to sparse polynomials.
+--
+-- >>> :set -XFlexibleContexts
+-- >>> denseToSparse (1 `Data.Semiring.plus` Data.Poly.X^2) :: Data.Poly.Sparse.UPoly Int
+-- 1 * X^2 + 1
+--
+-- @since 0.5.0.0
+denseToSparse :: (Eq a, Semiring a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a)) => Dense.Poly v a -> Sparse.Poly v a
+denseToSparse = Convert.denseToSparse'
+
+-- | Convert from sparse to dense polynomials.
+--
+-- >>> :set -XFlexibleContexts
+-- >>> sparseToDense (1 `Data.Semiring.plus` Data.Poly.Sparse.X^2) :: Data.Poly.UPoly Int
+-- 1 * X^2 + 0 * X + 1
+--
+-- @since 0.5.0.0
+sparseToDense :: (Semiring a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a)) => Sparse.Poly v a -> Dense.Poly v a
+sparseToDense = Convert.sparseToDense'
 #endif
diff --git a/src/Data/Poly/Sparse.hs b/src/Data/Poly/Sparse.hs
--- a/src/Data/Poly/Sparse.hs
+++ b/src/Data/Poly/Sparse.hs
@@ -4,20 +4,22 @@
 -- Licence:     BSD3
 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
 --
--- Sparse polynomials with 'Num' instance.
+-- Sparse polynomials with a 'Num' instance.
 --
+-- @since 0.3.0.0
+--
 
-{-# LANGUAGE CPP             #-}
-{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE DataKinds        #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE PatternSynonyms  #-}
 
 module Data.Poly.Sparse
   ( Poly
   , VPoly
   , UPoly
   , unPoly
-  , leading
-  -- * Num interface
   , toPoly
+  , leading
   , monomial
   , scale
   , pattern X
@@ -25,14 +27,125 @@
   , subst
   , deriv
   , integral
-#if MIN_VERSION_semirings(0,4,2)
-  -- * Polynomials over 'Field'
-  , gcdExt
-#endif
+  , quotRemFractional
+  , denseToSparse
+  , sparseToDense
   ) where
 
-import Data.Poly.Internal.Sparse
-#if MIN_VERSION_semirings(0,4,2)
-import Data.Poly.Internal.Sparse.Field (gcdExt)
-import Data.Poly.Internal.Sparse.GcdDomain ()
-#endif
+import Control.Arrow
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed.Sized as SU
+import qualified Data.Vector.Sized as SV
+
+import Data.Poly.Internal.Convert
+import Data.Poly.Internal.Multi (Poly, VPoly, UPoly, unPoly, leading)
+import qualified Data.Poly.Internal.Multi as Multi
+import Data.Poly.Internal.Multi.Field (quotRemFractional)
+import Data.Poly.Internal.Multi.GcdDomain ()
+
+-- | Make a 'Poly' from a list of (power, coefficient) pairs.
+--
+-- >>> :set -XOverloadedLists
+-- >>> toPoly [(0,1),(1,2),(2,3)] :: VPoly Integer
+-- 3 * X^2 + 2 * X + 1
+-- >>> toPoly [(0,0),(1,0),(2,0)] :: UPoly Int
+-- 0
+--
+-- @since 0.3.0.0
+toPoly
+  :: (Eq a, Num a, G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
+  => v (Word, a)
+  -> Poly v a
+toPoly = Multi.toMultiPoly . G.map (first SU.singleton)
+{-# INLINABLE toPoly #-}
+
+-- | Create a monomial from a power and a coefficient.
+--
+-- @since 0.3.0.0
+monomial
+  :: (Eq a, Num a, G.Vector v (SU.Vector 1 Word, a))
+  => Word
+  -> a
+  -> Poly v a
+monomial = Multi.monomial . SU.singleton
+{-# INLINABLE monomial #-}
+
+-- | Multiply a polynomial by a monomial, expressed as a power and a coefficient.
+--
+-- >>> scale 2 3 (X^2 + 1) :: UPoly Int
+-- 3 * X^4 + 3 * X^2
+--
+-- @since 0.3.0.0
+scale
+  :: (Eq a, Num a, G.Vector v (SU.Vector 1 Word, a))
+  => Word
+  -> a
+  -> Poly v a
+  -> Poly v a
+scale = Multi.scale . SU.singleton
+{-# INLINABLE scale #-}
+
+-- | The polynomial 'X'.
+--
+-- > X == monomial 1 1
+--
+-- @since 0.3.0.0
+pattern X
+  :: (Eq a, Num a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+pattern X = Multi.X
+
+-- | Evaluate the polynomial at a given point.
+--
+-- >>> eval (X^2 + 1 :: UPoly Int) 3
+-- 10
+--
+-- @since 0.3.0.0
+eval
+  :: (Num a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> a
+  -> a
+eval p = Multi.eval p . SV.singleton
+{-# INLINABLE eval #-}
+
+-- | Substitute another polynomial instead of 'X'.
+--
+-- >>> subst (X^2 + 1 :: UPoly Int) (X + 1 :: UPoly Int)
+-- 1 * X^2 + 2 * X + 2
+--
+-- @since 0.3.3.0
+subst
+  :: (Eq a, Num a, G.Vector v (SU.Vector 1 Word, a), G.Vector w (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly w a
+  -> Poly w a
+subst p = Multi.subst p . SV.singleton
+{-# INLINABLE subst #-}
+
+-- | Take the derivative of the polynomial.
+--
+-- >>> deriv (X^3 + 3 * X) :: UPoly Int
+-- 3 * X^2 + 3
+--
+-- @since 0.3.0.0
+deriv
+  :: (Eq a, Num a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+deriv = Multi.deriv 0
+{-# INLINABLE deriv #-}
+
+-- | Compute an indefinite integral of the polynomial,
+-- setting the constant term to zero.
+--
+-- >>> integral (3 * X^2 + 3) :: UPoly Double
+-- 1.0 * X^3 + 3.0 * X
+--
+-- @since 0.3.0.0
+integral
+  :: (Fractional a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+integral = Multi.integral 0
+{-# INLINABLE integral #-}
diff --git a/src/Data/Poly/Sparse/Laurent.hs b/src/Data/Poly/Sparse/Laurent.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Poly/Sparse/Laurent.hs
@@ -0,0 +1,137 @@
+-- |
+-- Module:      Data.Poly.Sparse.Laurent
+-- Copyright:   (c) 2020 Andrew Lelechenko
+-- Licence:     BSD3
+-- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
+--
+-- Sparse
+-- <https://en.wikipedia.org/wiki/Laurent_polynomial Laurent polynomials>.
+--
+-- @since 0.4.0.0
+
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE PatternSynonyms            #-}
+
+module Data.Poly.Sparse.Laurent
+  ( Laurent
+  , VLaurent
+  , ULaurent
+  , unLaurent
+  , toLaurent
+  , leading
+  , monomial
+  , scale
+  , pattern X
+  , (^-)
+  , eval
+  , subst
+  , deriv
+  ) where
+
+import Data.Euclidean (Field)
+import Data.Semiring (Semiring(..), Ring)
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed.Sized as SU
+import qualified Data.Vector.Sized as SV
+
+import Data.Poly.Internal.Multi.Laurent hiding (monomial, scale, pattern X, (^-), eval, subst, deriv)
+import qualified Data.Poly.Internal.Multi.Laurent as Multi
+import Data.Poly.Internal.Multi (Poly)
+
+-- | Create a monomial from a power and a coefficient.
+--
+-- @since 0.4.0.0
+monomial
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Int
+  -> a
+  -> Laurent v a
+monomial = Multi.monomial . SU.singleton
+{-# INLINABLE monomial #-}
+
+-- | Multiply a polynomial by a monomial, expressed as a power and a coefficient.
+--
+-- >>> scale 2 3 (X^-2 + 1) :: ULaurent Int
+-- 3 * X^2 + 3
+--
+-- @since 0.4.0.0
+scale
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Int
+  -> a
+  -> Laurent v a
+  -> Laurent v a
+scale = Multi.scale . SU.singleton
+{-# INLINABLE scale #-}
+
+-- | The polynomial 'X'.
+--
+-- > X == monomial 1 one
+--
+-- @since 0.4.0.0
+pattern X
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Laurent v a
+pattern X = Multi.X
+
+-- | Used to construct monomials with negative powers.
+--
+-- This operator can be applied only to monomials with unit coefficients,
+-- but is instrumental to express Laurent polynomials
+-- in a mathematical fashion:
+--
+-- >>> X^-3 :: ULaurent Int
+-- 1 * X^-3
+-- >>> X + 2 + 3 * (X^2)^-1 :: ULaurent Int
+-- 1 * X + 2 + 3 * X^-2
+--
+-- @since 0.4.0.0
+(^-)
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Laurent v a
+  -> Int
+  -> Laurent v a
+(^-) = (Multi.^-)
+
+-- | Evaluate the polynomial at a given point.
+--
+-- >>> eval (X^-2 + 1 :: ULaurent Double) 2
+-- 1.25
+--
+-- @since 0.4.0.0
+eval
+  :: (Field a, G.Vector v (SU.Vector 1 Word, a))
+  => Laurent v a
+  -> a
+  -> a
+eval p = Multi.eval p . SV.singleton
+{-# INLINABLE eval #-}
+
+-- | Substitute another polynomial instead of 'X'.
+--
+-- >>> import Data.Poly.Sparse (UPoly)
+-- >>> subst (Data.Poly.Sparse.X^2 + 1 :: UPoly Int) (X^-1 + 1 :: ULaurent Int)
+-- 2 + 2 * X^-1 + 1 * X^-2
+--
+-- @since 0.4.0.0
+subst
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a), G.Vector w (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Laurent w a
+  -> Laurent w a
+subst p = Multi.subst p . SV.singleton
+{-# INLINABLE subst #-}
+
+-- | Take the derivative of the polynomial.
+--
+-- >>> deriv (X^-3 + 3 * X) :: ULaurent Int
+-- 3 + (-3) * X^-4
+--
+-- @since 0.4.0.0
+deriv
+  :: (Eq a, Ring a, G.Vector v (SU.Vector 1 Word, a))
+  => Laurent v a
+  -> Laurent v a
+deriv = Multi.deriv 0
+{-# INLINABLE deriv #-}
diff --git a/src/Data/Poly/Sparse/Semiring.hs b/src/Data/Poly/Sparse/Semiring.hs
--- a/src/Data/Poly/Sparse/Semiring.hs
+++ b/src/Data/Poly/Sparse/Semiring.hs
@@ -4,10 +4,12 @@
 -- Licence:     BSD3
 -- Maintainer:  Andrew Lelechenko <andrew.lelechenko@gmail.com>
 --
--- Sparse polynomials with 'Semiring' instance.
+-- Sparse polynomials with a 'Semiring' instance.
 --
+-- @since 0.3.0.0
+--
 
-{-# LANGUAGE CPP              #-}
+{-# LANGUAGE DataKinds        #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE PatternSynonyms  #-}
 
@@ -16,90 +18,164 @@
   , VPoly
   , UPoly
   , unPoly
-  , leading
-  -- * Semiring interface
   , toPoly
+  , leading
   , monomial
   , scale
   , pattern X
   , eval
   , subst
   , deriv
-#if MIN_VERSION_semirings(0,5,0)
   , integral
-#endif
-#if MIN_VERSION_semirings(0,4,2)
-  -- * Polynomials over 'Field'
-  , gcdExt
-#endif
+  , denseToSparse
+  , sparseToDense
   ) where
 
-import Data.Semiring (Semiring)
+import Control.Arrow
+import Data.Euclidean (Field)
+import Data.Semiring (Semiring(..))
 import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed.Sized as SU
+import qualified Data.Vector.Sized as SV
 
-import Data.Poly.Internal.Sparse (Poly(..), VPoly, UPoly, leading)
-import qualified Data.Poly.Internal.Sparse as Sparse
-#if MIN_VERSION_semirings(0,4,2)
-import Data.Poly.Internal.Sparse.Field (gcdExt)
-import Data.Poly.Internal.Sparse.GcdDomain ()
-#endif
-#if MIN_VERSION_semirings(0,5,0)
-import Data.Euclidean (Field)
-#endif
+import qualified Data.Poly.Internal.Convert as Convert
+import qualified Data.Poly.Internal.Dense as Dense
+import Data.Poly.Internal.Multi (Poly, VPoly, UPoly, unPoly, leading)
+import qualified Data.Poly.Internal.Multi as Multi
+import Data.Poly.Internal.Multi.Field ()
+import Data.Poly.Internal.Multi.GcdDomain ()
 
--- | Make 'Poly' from a list of (power, coefficient) pairs.
--- (first element corresponds to a constant term).
+-- | Make a 'Poly' from a list of (power, coefficient) pairs.
 --
 -- >>> :set -XOverloadedLists
 -- >>> toPoly [(0,1),(1,2),(2,3)] :: VPoly Integer
 -- 3 * X^2 + 2 * X + 1
--- >>> S.toPoly [(0,0),(1,0),(2,0)] :: UPoly Int
+-- >>> toPoly [(0,0),(1,0),(2,0)] :: UPoly Int
 -- 0
-toPoly :: (Eq a, Semiring a, G.Vector v (Word, a)) => v (Word, a) -> Poly v a
-toPoly = Sparse.toPoly'
+--
+-- @since 0.3.0.0
+toPoly
+  :: (Eq a, Semiring a, G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
+  => v (Word, a)
+  -> Poly v a
+toPoly = Multi.toMultiPoly' . G.map (first SU.singleton)
+{-# INLINABLE toPoly #-}
 
 -- | Create a monomial from a power and a coefficient.
-monomial :: (Eq a, Semiring a, G.Vector v (Word, a)) => Word -> a -> Poly v a
-monomial = Sparse.monomial'
+--
+-- @since 0.3.0.0
+monomial
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Word
+  -> a
+  -> Poly v a
+monomial = Multi.monomial' . SU.singleton
+{-# INLINABLE monomial #-}
 
 -- | Multiply a polynomial by a monomial, expressed as a power and a coefficient.
 --
 -- >>> scale 2 3 (X^2 + 1) :: UPoly Int
 -- 3 * X^4 + 3 * X^2
-scale :: (Eq a, Semiring a, G.Vector v (Word, a)) => Word -> a -> Poly v a -> Poly v a
-scale = Sparse.scale'
+--
+-- @since 0.3.0.0
+scale
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Word
+  -> a
+  -> Poly v a
+  -> Poly v a
+scale = Multi.scale' . SU.singleton
+{-# INLINABLE scale #-}
 
--- | Create an identity polynomial.
-pattern X :: (Eq a, Semiring a, G.Vector v (Word, a), Eq (v (Word, a))) => Poly v a
-pattern X = Sparse.X'
+-- | The polynomial 'X'.
+--
+-- > X == monomial 1 one
+--
+-- @since 0.3.0.0
+pattern X
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+pattern X = Multi.X'
 
--- | Evaluate at a given point.
+-- | Evaluate the polynomial at a given point.
 --
 -- >>> eval (X^2 + 1 :: UPoly Int) 3
 -- 10
-eval :: (Semiring a, G.Vector v (Word, a)) => Poly v a -> a -> a
-eval = Sparse.eval'
+--
+-- @since 0.3.0.0
+eval
+  :: (Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> a
+  -> a
+eval p = Multi.eval' p . SV.singleton
+{-# INLINABLE eval #-}
 
 -- | Substitute another polynomial instead of 'X'.
 --
 -- >>> subst (X^2 + 1 :: UPoly Int) (X + 1 :: UPoly Int)
 -- 1 * X^2 + 2 * X + 2
-subst :: (Eq a, Semiring a, G.Vector v (Word, a), G.Vector w (Word, a)) => Poly v a -> Poly w a -> Poly w a
-subst = Sparse.subst'
+--
+-- @since 0.3.3.0
+subst
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a), G.Vector w (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly w a
+  -> Poly w a
+subst p = Multi.subst' p . SV.singleton
+{-# INLINABLE subst #-}
 
--- | Take a derivative.
+-- | Take the derivative of the polynomial.
 --
 -- >>> deriv (X^3 + 3 * X) :: UPoly Int
 -- 3 * X^2 + 3
-deriv :: (Eq a, Semiring a, G.Vector v (Word, a)) => Poly v a -> Poly v a
-deriv = Sparse.deriv'
+--
+-- @since 0.3.0.0
+deriv
+  :: (Eq a, Semiring a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+deriv = Multi.deriv' 0
+{-# INLINABLE deriv #-}
 
-#if MIN_VERSION_semirings(0,5,0)
--- | Compute an indefinite integral of a polynomial,
--- setting constant term to zero.
+-- | Compute an indefinite integral of the polynomial,
+-- setting the constant term to zero.
 --
 -- >>> integral (3 * X^2 + 3) :: UPoly Double
 -- 1.0 * X^3 + 3.0 * X
-integral :: (Eq a, Field a, G.Vector v (Word, a)) => Poly v a -> Poly v a
-integral = Sparse.integral'
-#endif
+--
+-- @since 0.3.2.0
+integral
+  :: (Field a, G.Vector v (SU.Vector 1 Word, a))
+  => Poly v a
+  -> Poly v a
+integral = Multi.integral' 0
+{-# INLINABLE integral #-}
+
+-- | Convert from dense to sparse polynomials.
+--
+-- >>> :set -XFlexibleContexts
+-- >>> denseToSparse (1 `Data.Semiring.plus` Data.Poly.X^2) :: Data.Poly.Sparse.UPoly Int
+-- 1 * X^2 + 1
+--
+-- @since 0.5.0.0
+denseToSparse
+  :: (Eq a, Semiring a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => Dense.Poly v a
+  -> Multi.Poly v a
+denseToSparse = Convert.denseToSparse'
+{-# INLINABLE denseToSparse #-}
+
+-- | Convert from sparse to dense polynomials.
+--
+-- >>> :set -XFlexibleContexts
+-- >>> sparseToDense (1 `Data.Semiring.plus` Data.Poly.Sparse.X^2) :: Data.Poly.UPoly Int
+-- 1 * X^2 + 0 * X + 1
+--
+-- @since 0.5.0.0
+sparseToDense
+  :: (Semiring a, G.Vector v a, G.Vector v (SU.Vector 1 Word, a))
+  => Multi.Poly v a
+  -> Dense.Poly v a
+sparseToDense = Convert.sparseToDense'
+{-# INLINABLE sparseToDense #-}
diff --git a/test/DFT.hs b/test/DFT.hs
new file mode 100644
--- /dev/null
+++ b/test/DFT.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE TypeOperators              #-}
+
+module DFT
+  ( testSuite
+  ) where
+
+import Data.Complex
+import Data.Mod.Word
+import Data.Poly.Semiring (UPoly, unPoly, toPoly, dft, inverseDft, dftMult)
+import qualified Data.Vector.Unboxed as U
+import GHC.TypeNats (KnownNat, natVal, type (+), type (^))
+import Test.Tasty
+import Test.Tasty.QuickCheck hiding (scale, numTests)
+
+import Dense ()
+
+testSuite :: TestTree
+testSuite = testGroup "DFT"
+  [ testGroup "dft matches reference"
+    [ dftMatchesRef (0 :: Mod (2 ^ 0 + 1))
+    , dftMatchesRef (2 :: Mod (2 ^ 1 + 1))
+    , dftMatchesRef (2 :: Mod (2 ^ 2 + 1))
+    , dftMatchesRef (3 :: Mod (2 ^ 4 + 1))
+    , dftMatchesRef (3 :: Mod (2 ^ 8 + 1))
+    ]
+  , testGroup "dft is invertible"
+    [ dftIsInvertible (0 :: Mod (2 ^ 0 + 1))
+    , dftIsInvertible (2 :: Mod (2 ^ 1 + 1))
+    , dftIsInvertible (2 :: Mod (2 ^ 2 + 1))
+    , dftIsInvertible (3 :: Mod (2 ^ 4 + 1))
+    , dftIsInvertible (3 :: Mod (2 ^ 8 + 1))
+    ]
+  , testProperty "dftMult matches reference" dftMultMatchesRef
+  ]
+
+dftMatchesRef :: KnownNat n1 => Mod n1 -> TestTree
+dftMatchesRef primRoot = testProperty (show n) $ do
+  xs <- U.replicateM n arbitrary
+  pure $ dft primRoot xs === dftRef primRoot xs
+  where
+    n = fromIntegral (natVal primRoot - 1)
+
+dftRef :: (Num a, U.Unbox a) => a -> U.Vector a -> U.Vector a
+dftRef primRoot xs = U.generate (U.length xs) $
+  \k -> sum (map (\j -> xs U.! j * primRoot ^ (j * k)) [0..n-1])
+  where
+    n = U.length xs
+
+dftIsInvertible :: KnownNat n1 => Mod n1 -> TestTree
+dftIsInvertible primRoot = testProperty (show n) $ do
+  xs <- U.replicateM n arbitrary
+  let ys = dft primRoot xs
+      zs = inverseDft primRoot ys
+  pure $ xs === zs
+  where
+    n = fromIntegral (natVal primRoot - 1)
+
+dftMultMatchesRef :: UPoly Int -> UPoly Int -> Property
+dftMultMatchesRef xs ys = zs === dftZs
+  where
+    xs', ys', dftZs' :: UPoly (Complex Double)
+    xs' = toPoly $ U.map fromIntegral $ unPoly xs
+    ys' = toPoly $ U.map fromIntegral $ unPoly ys
+    dftZs' = dftMult (\k -> cis (2 * pi / fromIntegral k)) xs' ys'
+
+    zs, dftZs :: UPoly (Complex Int)
+    zs  = toPoly $ U.map (:+ 0) $ unPoly $ xs * ys
+    dftZs  = toPoly $ U.map (\(x :+ y) -> round x :+ round y) $ unPoly dftZs'
diff --git a/test/Dense.hs b/test/Dense.hs
--- a/test/Dense.hs
+++ b/test/Dense.hs
@@ -1,155 +1,114 @@
 {-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
 {-# LANGUAGE FlexibleContexts           #-}
 {-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE ScopedTypeVariables        #-}
 
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
 module Dense
   ( testSuite
+  , ShortPoly(..)
   ) where
 
-import Prelude hiding (gcd, quotRem, rem)
-#if MIN_VERSION_semirings(0,4,2)
+import Prelude hiding (gcd, quotRem, quot, rem)
+import Control.Exception
 import Data.Euclidean (Euclidean(..), GcdDomain(..))
-#endif
 import Data.Int
-import Data.Maybe
+import Data.Mod.Word
 import Data.Poly
 import qualified Data.Poly.Semiring as S
 import Data.Proxy
-import Data.Semiring (Semiring)
+import Data.Semiring (Semiring(..))
 import qualified Data.Vector as V
 import qualified Data.Vector.Generic as G
 import qualified Data.Vector.Unboxed as U
 import Test.Tasty
 import Test.Tasty.QuickCheck hiding (scale, numTests)
-import Test.QuickCheck.Classes
 
 import Quaternion
-
-instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (Poly v a) where
-  arbitrary = S.toPoly . G.fromList <$> arbitrary
-  shrink = fmap (S.toPoly . G.fromList) . shrink . G.toList . unPoly
-
-#if MIN_VERSION_semirings(0,4,2)
-instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (PolyOverField (Poly v a)) where
-  arbitrary = PolyOverField . S.toPoly . G.fromList . (\xs -> take (length xs `mod` 10) xs) <$> arbitrary
-  shrink = fmap (PolyOverField . S.toPoly . G.fromList) . shrink . G.toList . unPoly . unPolyOverField
-#endif
-
-newtype ShortPoly a = ShortPoly { unShortPoly :: a }
-  deriving
-    ( Eq
-    , Show
-    , Semiring
-#if MIN_VERSION_semirings(0,4,2)
-    , GcdDomain
-    , Euclidean
-#endif
-    )
-
-instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (ShortPoly (Poly v a)) where
-  arbitrary = ShortPoly . S.toPoly . G.fromList . (\xs -> take (length xs `mod` 10) xs) <$> arbitrary
-  shrink = fmap (ShortPoly . S.toPoly . G.fromList) . shrink . G.toList . unPoly . unShortPoly
+import TestUtils
 
 testSuite :: TestTree
 testSuite = testGroup "Dense"
-    [ arithmeticTests
-    , otherTests
-    , lawsTests
-    , evalTests
-    , derivTests
-#if MIN_VERSION_semirings(0,4,2)
-    , gcdExtTests
-#endif
-    ]
+  [ arithmeticTests
+  , otherTests
+  , divideByZeroTests
+  , lawsTests
+  , evalTests
+  , derivTests
+  , patternTests
+  , conversionTests
+  ]
 
 lawsTests :: TestTree
 lawsTests = testGroup "Laws"
-  [ semiringTests
-  , ringTests
-  , numTests
-  , euclideanTests
-  , isListTests
-  , showTests
-  ]
+  $ semiringTests ++ ringTests ++ numTests ++ euclideanTests ++ gcdDomainTests ++ isListTests ++ showTests
 
-semiringTests :: TestTree
-semiringTests
-  = testGroup "Semiring"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [ semiringLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , semiringLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , semiringLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , semiringLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+semiringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+semiringTests =
+  [ mySemiringLaws (Proxy :: Proxy (UPoly ()))
+  , mySemiringLaws (Proxy :: Proxy (UPoly Int8))
+  , mySemiringLaws (Proxy :: Proxy (VPoly Integer))
+  , mySemiringLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
-
-ringTests :: TestTree
-ringTests
-  = testGroup "Ring"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_quickcheck_classes(0,6,1)
-    ringLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , ringLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , ringLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , ringLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+#else
+semiringTests = []
 #endif
-  ]
 
-numTests :: TestTree
-numTests
-  = testGroup "Num"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_quickcheck_classes(0,6,3)
-    numLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , numLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , numLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+ringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+ringTests =
+  [ myRingLaws (Proxy :: Proxy (UPoly ()))
+  , myRingLaws (Proxy :: Proxy (UPoly Int8))
+  , myRingLaws (Proxy :: Proxy (VPoly Integer))
+  , myRingLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
+  ]
+#else
+ringTests = []
 #endif
+
+numTests :: [TestTree]
+numTests =
+  [ myNumLaws (Proxy :: Proxy (UPoly Int8))
+  , myNumLaws (Proxy :: Proxy (VPoly Integer))
+  , myNumLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
 
-euclideanTests :: TestTree
-euclideanTests
-  = testGroup "Euclidean"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_semirings(0,4,2) && MIN_VERSION_quickcheck_classes(0,6,3)
-    gcdDomainLaws (Proxy :: Proxy (ShortPoly (Poly V.Vector Integer)))
-  , gcdDomainLaws (Proxy :: Proxy (PolyOverField (Poly V.Vector Rational)))
-  , euclideanLaws (Proxy :: Proxy (ShortPoly (Poly V.Vector Rational)))
+gcdDomainTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+gcdDomainTests =
+  [ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VPoly Integer)))
+  , myGcdDomainLaws (Proxy :: Proxy (ShortPoly (UPoly (Mod 3))))
+  , myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VPoly Rational)))
+  ]
+#else
+gcdDomainTests = []
 #endif
+
+euclideanTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+euclideanTests =
+  [ myEuclideanLaws (Proxy :: Proxy (ShortPoly (UPoly (Mod 3))))
+  , myEuclideanLaws (Proxy :: Proxy (ShortPoly (VPoly Rational)))
   ]
+#else
+euclideanTests = []
+#endif
 
-isListTests :: TestTree
-isListTests
-  = testGroup "IsList"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [ isListLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , isListLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , isListLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , isListLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+isListTests :: [TestTree]
+isListTests =
+  [ myIsListLaws (Proxy :: Proxy (UPoly ()))
+  , myIsListLaws (Proxy :: Proxy (UPoly Int8))
+  , myIsListLaws (Proxy :: Proxy (VPoly Integer))
+  , myIsListLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
 
-showTests :: TestTree
-showTests
-  = testGroup "Show"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_quickcheck_classes(0,6,0)
-    showLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , showLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , showLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , showLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
-#endif
+showTests :: [TestTree]
+showTests =
+  [ myShowLaws (Proxy :: Proxy (UPoly ()))
+  , myShowLaws (Proxy :: Proxy (UPoly Int8))
+  , myShowLaws (Proxy :: Proxy (VPoly Integer))
+  , myShowLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
 
 arithmeticTests :: TestTree
@@ -163,6 +122,9 @@
   , testProperty "multiplication matches reference" $
     \(xs :: [Int]) ys -> toPoly (V.fromList (mulRef xs ys)) ===
       toPoly (V.fromList xs) * toPoly (V.fromList ys)
+  , tenTimesLess $
+    testProperty "quotRemFractional matches quotRem" $
+    \(xs :: VPoly Rational) ys -> ys /= 0 ==> quotRemFractional xs ys === quotRem xs ys
   ]
 
 addRef :: Num a => [a] -> [a] -> [a]
@@ -199,18 +161,33 @@
     \p c -> c /= 0 ==> leading (monomial p c :: UPoly a) === Just (p, c)
   , testProperty "monomial matches reference" $
     \p (c :: a) -> monomial p c === toPoly (V.fromList (monomialRef p c))
-  , testProperty "scale matches multiplication by monomial" $
+  , tenTimesLess $
+    testProperty "scale matches multiplication by monomial" $
     \p c (xs :: UPoly a) -> scale p c xs === monomial p c * xs
+  , tenTimesLess $
+    testProperty "scale' matches multiplication by monomial'" $
+    \p c (xs :: UPoly a) -> S.scale p c xs === S.monomial p c * xs
   ]
 
 monomialRef :: Num a => Word -> a -> [a]
 monomialRef p c = replicate (fromIntegral p) 0 ++ [c]
 
+divideByZeroTests :: TestTree
+divideByZeroTests = testGroup "divideByZero"
+  [ testProperty "quotRem" $ testProp ((uncurry (+) .) . quotRem)
+  , testProperty "quot"    $ testProp quot
+  , testProperty "rem"     $ testProp rem
+  , testProperty "divide"  $ testProp divide
+  , testProperty "degree"  $ once $ degree (0 :: VPoly Rational) === 0
+  ]
+  where
+    testProp f xs = ioProperty ((== Left DivideByZero) <$> try (evaluate (xs `f` (0 :: VPoly Rational))))
+
 evalTests :: TestTree
 evalTests = testGroup "eval" $ concat
-  [ evalTestGroup  (Proxy :: Proxy (Poly U.Vector Int8))
-  , evalTestGroup  (Proxy :: Proxy (Poly V.Vector Integer))
-  , substTestGroup (Proxy :: Proxy (Poly U.Vector Int8))
+  [ evalTestGroup  (Proxy :: Proxy (UPoly Int8))
+  , evalTestGroup  (Proxy :: Proxy (VPoly Integer))
+  , substTestGroup (Proxy :: Proxy (UPoly Int8))
   ]
 
 evalTestGroup
@@ -220,18 +197,18 @@
   -> [TestTree]
 evalTestGroup _ =
   [ testProperty "eval (p + q) r = eval p r + eval q r" $
-    \p q r -> e (p + q) r === e p r + e q r
+    \(ShortPoly p) (ShortPoly q) r -> e (p + q) r === e p r + e q r
   , testProperty "eval (p * q) r = eval p r * eval q r" $
-    \p q r -> e (p * q) r === e p r * e q r
+    \(ShortPoly p) (ShortPoly q) r -> e (p * q) r === e p r * e q r
   , testProperty "eval x p = p" $
     \p -> e X p === p
   , testProperty "eval (monomial 0 c) p = c" $
     \c p -> e (monomial 0 c) p === c
 
   , testProperty "eval' (p + q) r = eval' p r + eval' q r" $
-    \p q r -> e' (p + q) r === e' p r + e' q r
+    \(ShortPoly p) (ShortPoly q) r -> e' (p + q) r === e' p r + e' q r
   , testProperty "eval' (p * q) r = eval' p r * eval' q r" $
-    \p q r -> e' (p * q) r === e' p r * e' q r
+    \(ShortPoly p) (ShortPoly q) r -> e' (p * q) r === e' p r * e' q r
   , testProperty "eval' x p = p" $
     \p -> e' S.X p === p
   , testProperty "eval' (S.monomial 0 c) p = c" $
@@ -250,14 +227,16 @@
   => Proxy (Poly v a)
   -> [TestTree]
 substTestGroup _ =
-  [ testProperty "subst (p + q) r = subst p r + subst q r" $
-    \p q r -> e (p + q) r === e p r + e q r
+  [ tenTimesLess $ tenTimesLess $ tenTimesLess $
+    testProperty "subst (p + q) r = subst p r + subst q r" $
+    \p q (ShortPoly r) -> e (p + q) r === e p r + e q r
   , testProperty "subst x p = p" $
     \p -> e X p === p
   , testProperty "subst (monomial 0 c) p = monomial 0 c" $
     \c p -> e (monomial 0 c) p === monomial 0 c
-  , testProperty "subst' (p + q) r = subst' p r + subst' q r" $
-    \p q r -> e' (p + q) r === e' p r + e' q r
+  , tenTimesLess $ tenTimesLess $ tenTimesLess $
+    testProperty "subst' (p + q) r = subst' p r + subst' q r" $
+    \p q (ShortPoly r) -> e' (p + q) r === e' p r + e' q r
   , testProperty "subst' x p = p" $
     \p -> e' S.X p === p
   , testProperty "subst' (S.monomial 0 c) p = S.monomial 0 c" $
@@ -272,41 +251,51 @@
 derivTests :: TestTree
 derivTests = testGroup "deriv"
   [ testProperty "deriv = S.deriv" $
-    \(p :: Poly V.Vector Integer) -> deriv p === S.deriv p
-#if MIN_VERSION_semirings(0,5,0)
+    \(p :: VPoly Integer) -> deriv p === S.deriv p
   , testProperty "integral = S.integral" $
-    \(p :: Poly V.Vector Rational) -> integral p === S.integral p
-#endif
+    \(p :: VPoly Rational) -> integral p === S.integral p
   , testProperty "deriv . integral = id" $
-    \(p :: Poly V.Vector Rational) -> deriv (integral p) === p
+    \(p :: VPoly Rational) -> deriv (integral p) === p
   , testProperty "deriv c = 0" $
-    \c -> deriv (monomial 0 c :: Poly V.Vector Int) === 0
+    \c -> deriv (monomial 0 c :: UPoly Int) === 0
   , testProperty "deriv cX = c" $
-    \c -> deriv (monomial 0 c * X :: Poly V.Vector Int) === monomial 0 c
+    \c -> deriv (monomial 0 c * X :: UPoly Int) === monomial 0 c
   , testProperty "deriv (p + q) = deriv p + deriv q" $
-    \p q -> deriv (p + q) === (deriv p + deriv q :: Poly V.Vector Int)
+    \p q -> deriv (p + q) === (deriv p + deriv q :: UPoly Int)
   , testProperty "deriv (p * q) = p * deriv q + q * deriv p" $
-    \p q -> deriv (p * q) === (p * deriv q + q * deriv p :: Poly V.Vector Int)
-  , testProperty "deriv (subst p q) = deriv q * subst (deriv p) q" $
-    \(p :: Poly V.Vector Int) (q :: Poly U.Vector Int) ->
+    \p q -> deriv (p * q) === (p * deriv q + q * deriv p :: UPoly Int)
+  , tenTimesLess $ tenTimesLess $ tenTimesLess $
+    testProperty "deriv (subst p q) = deriv q * subst (deriv p) q" $
+    \(ShortPoly (p :: UPoly Int)) (ShortPoly (q :: UPoly Int)) ->
       deriv (subst p q) === deriv q * subst (deriv p) q
   ]
 
-#if MIN_VERSION_semirings(0,4,2)
-gcdExtTests :: TestTree
-gcdExtTests = localOption (QuickCheckMaxSize 12) $ testGroup "gcdExt"
-  [ testProperty "gcdExt == S.gcdExt" $
-    \(a :: Poly V.Vector Rational) b ->
-      gcdExt a b === S.gcdExt a b
-  , testProperty "g == as (mod b) for gcdExt" $
-    \(a :: Poly V.Vector Rational) b -> b /= 0 ==>
-      uncurry ((. flip rem b) . (===) . flip rem b) ((* a) <$> gcdExt a b)
-  , testProperty "fst . gcdExt == gcd (mod units)" $
-    \(a :: Poly V.Vector Rational) b ->
-      fst (gcdExt a b) `sameUpToUnits` gcd a b
+patternTests :: TestTree
+patternTests = testGroup "pattern"
+  [ testProperty "X  :: UPoly Int" $ once $
+    case (monomial 1 1 :: UPoly Int) of X -> True; _ -> False
+  , testProperty "X  :: UPoly Int" $ once $
+    (X :: UPoly Int) === monomial 1 1
+  , testProperty "X' :: UPoly Int" $ once $
+    case (S.monomial 1 1 :: UPoly Int) of S.X -> True; _ -> False
+  , testProperty "X' :: UPoly Int" $ once $
+    (S.X :: UPoly Int) === S.monomial 1 1
+  , testProperty "X' :: UPoly ()" $ once $
+    case (zero :: UPoly ()) of S.X -> True; _ -> False
+  , testProperty "X' :: UPoly ()" $ once $
+    (S.X :: UPoly ()) === zero
   ]
 
-sameUpToUnits :: (Eq a, GcdDomain a) => a -> a -> Bool
-sameUpToUnits x y = x == y ||
-  isJust (x `divide` y) && isJust (y `divide` x)
+conversionTests :: TestTree
+conversionTests = testGroup "conversions"
+  [ testProperty "toPoly . unPoly = id" $
+    \(xs :: UPoly Int8) -> xs === toPoly (unPoly xs)
+  , testProperty "S.toPoly . S.unPoly = id" $
+    \(xs :: UPoly Int8) -> xs === S.toPoly (S.unPoly xs)
+#ifdef SupportSparse
+  , testProperty "sparseToDense . denseToSparse = id" $
+    \(xs :: UPoly Int8) -> xs === sparseToDense (denseToSparse xs)
+  , testProperty "sparseToDense' . denseToSparse' = id" $
+    \(xs :: UPoly Int8) -> xs === S.sparseToDense (S.denseToSparse xs)
 #endif
+  ]
diff --git a/test/DenseLaurent.hs b/test/DenseLaurent.hs
new file mode 100644
--- /dev/null
+++ b/test/DenseLaurent.hs
@@ -0,0 +1,197 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+
+module DenseLaurent
+  ( testSuite
+  ) where
+
+import Prelude hiding (gcd, quotRem, quot, rem)
+import Control.Exception
+import Data.Euclidean (GcdDomain(..), Field)
+import Data.Int
+import qualified Data.Poly
+import Data.Poly.Laurent
+import Data.Proxy
+import Data.Semiring (Semiring(..))
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed as U
+import Test.Tasty
+import Test.Tasty.QuickCheck hiding (scale, numTests)
+
+import Quaternion
+import TestUtils
+
+testSuite :: TestTree
+testSuite = testGroup "DenseLaurent"
+  [ otherTests
+  , divideByZeroTests
+  , lawsTests
+  , evalTests
+  , derivTests
+  , patternTests
+  ]
+
+lawsTests :: TestTree
+lawsTests = testGroup "Laws"
+  $ semiringTests ++ ringTests ++ numTests ++ gcdDomainTests ++ showTests
+
+semiringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+semiringTests =
+  [ mySemiringLaws (Proxy :: Proxy (ULaurent ()))
+  , mySemiringLaws (Proxy :: Proxy (ULaurent Int8))
+  , mySemiringLaws (Proxy :: Proxy (VLaurent Integer))
+  , mySemiringLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+#else
+semiringTests = []
+#endif
+
+ringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+ringTests =
+  [ myRingLaws (Proxy :: Proxy (ULaurent ()))
+  , myRingLaws (Proxy :: Proxy (ULaurent Int8))
+  , myRingLaws (Proxy :: Proxy (VLaurent Integer))
+  , myRingLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+#else
+ringTests = []
+#endif
+
+numTests :: [TestTree]
+numTests =
+  [ myNumLaws (Proxy :: Proxy (ULaurent Int8))
+  , myNumLaws (Proxy :: Proxy (VLaurent Integer))
+  , myNumLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+
+gcdDomainTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+gcdDomainTests =
+  [ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VLaurent Integer)))
+  , myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VLaurent Rational)))
+  ]
+#else
+gcdDomainTests = []
+#endif
+
+showTests :: [TestTree]
+showTests =
+  [ myShowLaws (Proxy :: Proxy (ULaurent ()))
+  , myShowLaws (Proxy :: Proxy (ULaurent Int8))
+  , myShowLaws (Proxy :: Proxy (VLaurent Integer))
+  , myShowLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+
+otherTests :: TestTree
+otherTests = testGroup "other" $ concat
+  [ otherTestGroup (Proxy :: Proxy Int8)
+  , otherTestGroup (Proxy :: Proxy (Quaternion Int))
+  ]
+
+otherTestGroup
+  :: forall a.
+     (Eq a, Show a, Semiring a, Num a, Arbitrary a, U.Unbox a, G.Vector U.Vector a)
+  => Proxy a
+  -> [TestTree]
+otherTestGroup _ =
+  [ testProperty "leading p 0 == Nothing" $
+    \p -> leading (monomial p 0 :: ULaurent a) === Nothing
+  , testProperty "leading . monomial = id" $
+    \p c -> c /= 0 ==> leading (monomial p c :: ULaurent a) === Just (p, c)
+  , tenTimesLess $
+    testProperty "scale matches multiplication by monomial" $
+    \p c (xs :: ULaurent a) -> scale p c xs === monomial p c * xs
+  , tenTimesLess $
+    testProperty "toLaurent . unLaurent" $
+    \(xs :: ULaurent a) -> uncurry toLaurent (unLaurent xs) === xs
+  ]
+
+divideByZeroTests :: TestTree
+divideByZeroTests = testGroup "divideByZero"
+  [ testProperty "divide"  $ testProp divide
+  ]
+  where
+    testProp f xs = ioProperty ((== Left DivideByZero) <$> try (evaluate (xs `f` (0 :: VLaurent Rational))))
+
+evalTests :: TestTree
+evalTests = testGroup "eval" $ concat
+  [ evalTestGroup  (Proxy :: Proxy (VLaurent Rational))
+  , substTestGroup (Proxy :: Proxy (ULaurent Int8))
+  ]
+
+evalTestGroup
+  :: forall v a.
+     (Eq a, Field a, Arbitrary a, Show a, Eq (v a), Show (v a), G.Vector v a)
+  => Proxy (Laurent v a)
+  -> [TestTree]
+evalTestGroup _ =
+  [ testProperty "eval (p + q) r = eval p r + eval q r" $
+    \(ShortPoly p) (ShortPoly q) r -> e (p `plus` q) r === e p r `plus` e q r
+  , testProperty "eval (p * q) r = eval p r * eval q r" $
+    \(ShortPoly p) (ShortPoly q) r -> e (p `times` q) r === e p r `times` e q r
+  , testProperty "eval x p = p" $
+    \p -> e X p === p
+  , testProperty "eval (monomial 0 c) p = c" $
+    \c p -> e (monomial 0 c) p === c
+  ]
+  where
+    e :: Laurent v a -> a -> a
+    e = eval
+
+substTestGroup
+  :: forall v a.
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v a), Show (v a), G.Vector v a)
+  => Proxy (Laurent v a)
+  -> [TestTree]
+substTestGroup _ =
+  [ tenTimesLess $ tenTimesLess $ tenTimesLess $
+    testProperty "subst (p + q) r = subst p r + subst q r" $
+    \p q (ShortPoly r) -> e (p + q) r === e p r + e q r
+  , testProperty "subst x p = p" $
+    \p -> e Data.Poly.X p === p
+  , testProperty "subst (monomial 0 c) p = monomial 0 c" $
+    \c p -> e (Data.Poly.monomial 0 c) p === monomial 0 c
+  ]
+  where
+    e :: Data.Poly.Poly v a -> Laurent v a -> Laurent v a
+    e = subst
+
+derivTests :: TestTree
+derivTests = testGroup "deriv"
+  [ testProperty "deriv c = 0" $
+    \c -> deriv (monomial 0 c :: ULaurent Int) === 0
+  , testProperty "deriv cX = c" $
+    \c -> deriv (monomial 0 c * X :: ULaurent Int) === monomial 0 c
+  , testProperty "deriv (p + q) = deriv p + deriv q" $
+    \p q -> deriv (p + q) === (deriv p + deriv q :: ULaurent Int)
+  , testProperty "deriv (p * q) = p * deriv q + q * deriv p" $
+    \p q -> deriv (p * q) === (p * deriv q + q * deriv p :: ULaurent Int)
+  , tenTimesLess $ tenTimesLess $ tenTimesLess $
+    testProperty "deriv (subst p q) = deriv q * subst (deriv p) q" $
+    \(ShortPoly (p :: Data.Poly.UPoly Int)) (ShortPoly (q :: ULaurent Int)) ->
+      deriv (subst p q) === deriv q * subst (Data.Poly.deriv p) q
+  ]
+
+patternTests :: TestTree
+patternTests = testGroup "pattern"
+  [ testProperty "X  :: ULaurent Int" $ once $
+    case (monomial 1 1 :: ULaurent Int) of X -> True; _ -> False
+  , testProperty "X  :: ULaurent Int" $ once $
+    (X :: ULaurent Int) === monomial 1 1
+  , testProperty "X :: ULaurent ()" $ once $
+    case (zero :: ULaurent ()) of X -> True; _ -> False
+  , testProperty "X :: ULaurent ()" $ once $
+    (X :: ULaurent ()) === zero
+  , testProperty "X^-k" $
+    \(NonNegative j) k -> ((X^j)^-k :: ULaurent Int) === monomial (- j * k) 1
+  , testProperty "^-" $
+    \(p :: ULaurent Int) (NonNegative k) -> ioProperty $ do
+      et <- try (evaluate (p^-k)) :: IO (Either PatternMatchFail (ULaurent Int))
+      pure $ case et of
+        Left{}  -> True
+        Right t -> p^k * t == one
+  ]
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,12 +1,30 @@
+{-# LANGUAGE CPP #-}
+
 module Main where
 
 import Test.Tasty
 
-import qualified Dense as Dense
-import qualified Sparse as Sparse
+import qualified Dense
+import qualified DenseLaurent
+import qualified DFT
+import qualified Orthogonal
+#ifdef SupportSparse
+import qualified Multi
+import qualified MultiLaurent
+import qualified Sparse
+import qualified SparseLaurent
+#endif
 
 main :: IO ()
 main = defaultMain $ testGroup "All"
     [ Dense.testSuite
+    , DenseLaurent.testSuite
+    , DFT.testSuite
+    , Orthogonal.testSuite
+#ifdef SupportSparse
     , Sparse.testSuite
+    , SparseLaurent.testSuite
+    , Multi.testSuite
+    , MultiLaurent.testSuite
+#endif
     ]
diff --git a/test/Multi.hs b/test/Multi.hs
new file mode 100644
--- /dev/null
+++ b/test/Multi.hs
@@ -0,0 +1,320 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+module Multi
+  ( testSuite
+  ) where
+
+import Prelude hiding (gcd, quotRem, rem)
+import Control.Exception
+import Data.Euclidean (GcdDomain(..))
+import Data.Function
+import Data.Int
+import Data.List (groupBy, sortOn)
+import Data.Mod.Word
+import Data.Proxy
+import Data.Semiring (Semiring(..))
+import qualified Data.Vector as V
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Sized as SG
+import qualified Data.Vector.Sized as SV
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Sized as SU
+import Test.Tasty
+import Test.Tasty.QuickCheck hiding (scale, numTests)
+
+import Data.Poly.Multi
+import qualified Data.Poly.Multi.Semiring as S
+
+import Quaternion
+import TestUtils
+
+testSuite :: TestTree
+testSuite = testGroup "Multi"
+  [ arithmeticTests
+  , otherTests
+  , divideByZeroTests
+  , lawsTests
+  , evalTests
+  , derivTests
+  , patternTests
+  , conversionTests
+  ]
+
+lawsTests :: TestTree
+lawsTests = testGroup "Laws"
+  $ semiringTests ++ ringTests ++ numTests ++ gcdDomainTests ++ isListTests ++ showTests
+
+semiringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+semiringTests =
+  [ mySemiringLaws (Proxy :: Proxy (UMultiPoly 3 ()))
+  , mySemiringLaws (Proxy :: Proxy (ShortPoly (UMultiPoly 2 Int8)))
+  , mySemiringLaws (Proxy :: Proxy (ShortPoly (VMultiPoly 2 Integer)))
+  , tenTimesLess
+  $ mySemiringLaws (Proxy :: Proxy (ShortPoly (UMultiPoly 2 (Quaternion Int))))
+  ]
+#else
+semiringTests = []
+#endif
+
+ringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+ringTests =
+  [ myRingLaws (Proxy :: Proxy (UMultiPoly 3 ()))
+  , myRingLaws (Proxy :: Proxy (UMultiPoly 3 Int8))
+  , myRingLaws (Proxy :: Proxy (VMultiPoly 3 Integer))
+  , myRingLaws (Proxy :: Proxy (UMultiPoly 3 (Quaternion Int)))
+  ]
+#else
+ringTests = []
+#endif
+
+numTests :: [TestTree]
+numTests =
+  [ myNumLaws (Proxy :: Proxy (ShortPoly (UMultiPoly 2 Int8)))
+  , myNumLaws (Proxy :: Proxy (ShortPoly (VMultiPoly 2 Integer)))
+  , tenTimesLess
+  $ myNumLaws (Proxy :: Proxy (ShortPoly (UMultiPoly 2 (Quaternion Int))))
+  ]
+
+gcdDomainTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+gcdDomainTests =
+  [ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VMultiPoly 3 Integer)))
+  , tenTimesLess
+  $ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VMultiPoly 3 (Mod 3))))
+  , tenTimesLess
+  $ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VMultiPoly 3 Rational)))
+  ]
+#else
+gcdDomainTests = []
+#endif
+
+isListTests :: [TestTree]
+isListTests =
+  [ myIsListLaws (Proxy :: Proxy (UMultiPoly 3 ()))
+  , myIsListLaws (Proxy :: Proxy (UMultiPoly 3 Int8))
+  , myIsListLaws (Proxy :: Proxy (VMultiPoly 3 Integer))
+  , tenTimesLess
+  $ myIsListLaws (Proxy :: Proxy (UMultiPoly 3 (Quaternion Int)))
+  ]
+
+showTests :: [TestTree]
+showTests =
+  [ myShowLaws (Proxy :: Proxy (UMultiPoly 4 ()))
+  , myShowLaws (Proxy :: Proxy (UMultiPoly 4 Int8))
+  , myShowLaws (Proxy :: Proxy (VMultiPoly 4 Integer))
+  , tenTimesLess
+  $ myShowLaws (Proxy :: Proxy (UMultiPoly 4 (Quaternion Int)))
+  ]
+
+arithmeticTests :: TestTree
+arithmeticTests = testGroup "Arithmetic"
+  [ testProperty "addition matches reference" $
+    \(xs :: [(SU.Vector 3 Word, Int)]) ys -> toMultiPoly (V.fromList (addRef xs ys)) ===
+      toMultiPoly (V.fromList xs) + toMultiPoly (V.fromList ys)
+  , testProperty "subtraction matches reference" $
+    \(xs :: [(SU.Vector 3 Word, Int)]) ys -> toMultiPoly (V.fromList (subRef xs ys)) ===
+      toMultiPoly (V.fromList xs) - toMultiPoly (V.fromList ys)
+  , tenTimesLess $
+    testProperty "multiplication matches reference" $
+    \(xs :: [(SU.Vector 3 Word, Int)]) ys -> toMultiPoly (V.fromList (mulRef xs ys)) ===
+      toMultiPoly (V.fromList xs) * toMultiPoly (V.fromList ys)
+  ]
+
+addRef :: (Num a, Ord t) => [(t, a)] -> [(t, a)] -> [(t, a)]
+addRef [] ys = ys
+addRef xs [] = xs
+addRef xs@((xp, xc) : xs') ys@((yp, yc) : ys') =
+  case xp `compare` yp of
+    LT -> (xp, xc) : addRef xs' ys
+    EQ -> (xp, xc + yc) : addRef xs' ys'
+    GT -> (yp, yc) : addRef xs ys'
+
+subRef :: (Num a, Ord t) => [(t, a)] -> [(t, a)] -> [(t, a)]
+subRef [] ys = map (fmap negate) ys
+subRef xs [] = xs
+subRef xs@((xp, xc) : xs') ys@((yp, yc) : ys') =
+  case xp `compare` yp of
+    LT -> (xp, xc) : subRef xs' ys
+    EQ -> (xp, xc - yc) : subRef xs' ys'
+    GT -> (yp, negate yc) : subRef xs ys'
+
+mulRef :: (Num a, Ord t, Num t) => [(t, a)] -> [(t, a)] -> [(t, a)]
+mulRef xs ys
+  = map (\ws -> (fst (head ws), sum (map snd ws)))
+  $ groupBy ((==) `on` fst)
+  $ sortOn fst
+  $ [ (xp + yp, xc * yc) | (xp, xc) <- xs, (yp, yc) <- ys ]
+
+otherTests :: TestTree
+otherTests = testGroup "other" $ concat
+  [ otherTestGroup (Proxy :: Proxy Int8)
+  , otherTestGroup (Proxy :: Proxy (Quaternion Int))
+  ]
+
+otherTestGroup
+  :: forall a.
+     (Eq a, Show a, Semiring a, Num a, Arbitrary a, U.Unbox a, G.Vector U.Vector a)
+  => Proxy a
+  -> [TestTree]
+otherTestGroup _ =
+  [ testProperty "monomial matches reference" $
+    \(ps :: SU.Vector 3 Word) (c :: a) -> monomial ps c === toMultiPoly (V.fromList (monomialRef ps c))
+  , tenTimesLess $
+    testProperty "scale matches multiplication by monomial" $
+    \ps c (xs :: UMultiPoly 3 a) -> scale ps c xs === monomial ps c * xs
+  , tenTimesLess $
+    testProperty "scale' matches multiplication by monomial" $
+    \ps c (xs :: UMultiPoly 3 a) -> S.scale ps c xs === S.monomial ps c * xs
+  ]
+
+monomialRef :: Num a => t -> a -> [(t, a)]
+monomialRef p c = [(p, c)]
+
+divideByZeroTests :: TestTree
+divideByZeroTests = testGroup "divideByZero"
+  [ testProperty "divide"  $ testProp divide
+  ]
+  where
+    testProp f xs = ioProperty ((== Left DivideByZero) <$> try (evaluate (xs `f` (0 :: VMultiPoly 3 Rational))))
+
+evalTests :: TestTree
+evalTests = testGroup "eval" $ concat
+  [ evalTestGroup  (Proxy :: Proxy (UMultiPoly 3 Int8))
+  , evalTestGroup  (Proxy :: Proxy (VMultiPoly 3 Integer))
+  , substTestGroup (Proxy :: Proxy (UMultiPoly 3 Int8))
+  ]
+
+evalTestGroup
+  :: forall v a.
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (SU.Vector 3 Word, a)), Show (v (SU.Vector 3 Word, a)), G.Vector v (SU.Vector 3 Word, a))
+  => Proxy (MultiPoly v 3 a)
+  -> [TestTree]
+evalTestGroup _ =
+  [ testProperty "eval (p + q) rs = eval p rs + eval q rs" $
+    \(ShortPoly p) (ShortPoly q) rs -> e (p + q) rs === e p rs + e q rs
+  , testProperty "eval (p * q) rs = eval p rs * eval q rs" $
+    \(ShortPoly p) (ShortPoly q) rs -> e (p * q) rs === e p rs * e q rs
+  , testProperty "eval x p = p" $
+    \p -> e X (SV.fromTuple (p, undefined, undefined)) === p
+  , testProperty "eval (monomial 0 c) p = c" $
+    \c ps -> e (monomial 0 c) ps === c
+
+  , testProperty "eval' (p + q) rs = eval' p rs + eval' q rs" $
+    \(ShortPoly p) (ShortPoly q) rs -> e' (p + q) rs === e' p rs + e' q rs
+  , testProperty "eval' (p * q) rs = eval' p rs * eval' q rs" $
+    \(ShortPoly p) (ShortPoly q) rs -> e' (p * q) rs === e' p rs * e' q rs
+  , testProperty "eval' x p = p" $
+    \p -> e' S.X (SV.fromTuple (p, undefined, undefined)) === p
+  , testProperty "eval' (monomial 0 c) p = c" $
+    \c ps -> e' (monomial 0 c) ps === c
+  ]
+  where
+    e :: MultiPoly v 3 a -> SV.Vector 3 a -> a
+    e = eval
+    e' :: MultiPoly v 3 a -> SV.Vector 3 a -> a
+    e' = S.eval
+
+substTestGroup
+  :: forall v a.
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (SU.Vector 3 Word, a)), Show (v (SU.Vector 3 Word, a)), G.Vector v (SU.Vector 3 Word, a))
+  => Proxy (MultiPoly v 3 a)
+  -> [TestTree]
+substTestGroup _ =
+  [ testProperty "subst x p = p" $
+    \p -> e X (SV.fromTuple (p, undefined, undefined)) === p
+  , testProperty "subst (monomial 0 c) ps = monomial 0 c" $
+    \c ps -> e (monomial 0 c) ps === monomial 0 c
+  , testProperty "subst' x p = p" $
+    \p -> e' S.X (SV.fromTuple (p, undefined, undefined)) === p
+  , testProperty "subst' (S.monomial 0 c) ps = S.monomial 0 c" $
+    \c ps -> e' (S.monomial 0 c) ps === S.monomial 0 c
+  ]
+  where
+    e :: MultiPoly v 3 a -> SV.Vector 3 (MultiPoly v 3 a) -> MultiPoly v 3 a
+    e = subst
+    e' :: MultiPoly v 3 a -> SV.Vector 3 (MultiPoly v 3 a) -> MultiPoly v 3 a
+    e' = S.subst
+
+derivTests :: TestTree
+derivTests = testGroup "deriv"
+  [ testProperty "deriv = S.deriv" $
+    \k (p :: VMultiPoly 3 Integer) -> deriv k p === S.deriv k p
+  , testProperty "integral = S.integral" $
+    \k (p :: VMultiPoly 3 Rational) -> integral k p === S.integral k p
+  , testProperty "deriv . integral = id" $
+    \k (p :: VMultiPoly 3 Rational) ->
+      deriv k (integral k p) === p
+  , testProperty "deriv c = 0" $
+    \k c ->
+      deriv k (monomial 0 c :: UMultiPoly 3 Int) === 0
+  , testProperty "deriv cX = c" $
+    \c ->
+      deriv 0 (monomial 0 c * X :: UMultiPoly 3 Int) === monomial 0 c
+  , testProperty "deriv (p + q) = deriv p + deriv q" $
+    \k p q ->
+      deriv k (p + q) === (deriv k p + deriv k q :: UMultiPoly 3 Int)
+  , testProperty "deriv (p * q) = p * deriv q + q * deriv p" $
+    \k p q ->
+      deriv k (p * q) === (p * deriv k q + q * deriv k p :: UMultiPoly 3 Int)
+  ]
+
+patternTests :: TestTree
+patternTests = testGroup "pattern"
+  [ testProperty "X  :: UMultiPoly Int" $ once $
+    case (monomial 1 1 :: UMultiPoly 1 Int) of X -> True; _ -> False
+  , testProperty "X  :: UMultiPoly Int" $ once $
+    (X :: UMultiPoly 1 Int) === monomial 1 1
+  , testProperty "S.X  :: UMultiPoly Int8" $ once $
+    case (S.monomial 1 1 :: UMultiPoly 1 Int8) of S.X -> True; _ -> False
+  , testProperty "S.X  :: UMultiPoly Int8" $ once $
+    (S.X :: UMultiPoly 1 Int8) === S.monomial 1 1
+  , testProperty "X :: UMultiPoly ()" $ once $
+    case (zero :: UMultiPoly 1 ()) of S.X -> True; _ -> False
+  , testProperty "X :: UMultiPoly ()" $ once $
+    (S.X :: UMultiPoly 1 ()) === zero
+
+  , testProperty "Y  :: UMultiPoly Int" $ once $
+    case (monomial (SG.fromTuple (0, 1)) 1 :: UMultiPoly 2 Int) of Y -> True; _ -> False
+  , testProperty "Y  :: UMultiPoly Int" $ once $
+    (Y :: UMultiPoly 2 Int) === monomial (SG.fromTuple (0, 1)) 1
+  , testProperty "S.Y  :: UMultiPoly Int8" $ once $
+    case (S.monomial (SG.fromTuple (0, 1)) 1 :: UMultiPoly 2 Int8) of S.Y -> True; _ -> False
+  , testProperty "S.Y  :: UMultiPoly Int8" $ once $
+    (S.Y :: UMultiPoly 2 Int8) === S.monomial (SG.fromTuple (0, 1)) 1
+  , testProperty "Y :: UMultiPoly ()" $ once $
+    case (zero :: UMultiPoly 2 ()) of S.Y -> True; _ -> False
+  , testProperty "Y :: UMultiPoly ()" $ once $
+    (S.Y :: UMultiPoly 2 ()) === zero
+
+  , testProperty "Z  :: UMultiPoly Int" $ once $
+    case (monomial (SG.fromTuple (0, 0, 1)) 1 :: UMultiPoly 3 Int) of Z -> True; _ -> False
+  , testProperty "Z  :: UMultiPoly Int" $ once $
+    (Z :: UMultiPoly 3 Int) === monomial (SG.fromTuple (0, 0, 1)) 1
+  , testProperty "S.Z  :: UMultiPoly Int8" $ once $
+    case (S.monomial (SG.fromTuple (0, 0, 1)) 1 :: UMultiPoly 3 Int) of S.Z -> True; _ -> False
+  , testProperty "S.Z  :: UMultiPoly Int" $ once $
+    (S.Z :: UMultiPoly 3 Int) === S.monomial (SG.fromTuple (0, 0, 1)) 1
+  , testProperty "Z :: UMultiPoly ()" $ once $
+    case (zero :: UMultiPoly 3 ()) of S.Z -> True; _ -> False
+  , testProperty "Z :: UMultiPoly ()" $ once $
+    (S.Z :: UMultiPoly 3 ()) === zero
+  ]
+
+conversionTests :: TestTree
+conversionTests = testGroup "conversions"
+  [ testProperty "unsegregate . segregate = id" $
+    \(xs :: UMultiPoly 3 Int8) -> xs === unsegregate (segregate xs)
+  , testProperty "segregate . unsegregate = id" $
+    \xs -> xs === segregate (unsegregate xs :: UMultiPoly 3 Int8)
+  , testProperty "toMultiPoly . unMultiPoly = id" $
+    \(xs :: UMultiPoly 3 Int8) -> xs === toMultiPoly (unMultiPoly xs)
+  , testProperty "S.toMultiPoly . S.unMultiPoly = id" $
+    \(xs :: UMultiPoly 3 Int8) -> xs === S.toMultiPoly (S.unMultiPoly xs)
+  ]
diff --git a/test/MultiLaurent.hs b/test/MultiLaurent.hs
new file mode 100644
--- /dev/null
+++ b/test/MultiLaurent.hs
@@ -0,0 +1,235 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+module MultiLaurent
+  ( testSuite
+  ) where
+
+import Prelude hiding (gcd, quotRem, quot, rem)
+import Control.Exception
+import Data.Euclidean (GcdDomain(..), Field)
+import Data.Int
+import qualified Data.Poly.Multi
+import Data.Poly.Multi.Laurent
+import Data.Proxy
+import Data.Semiring (Semiring(..))
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Generic.Sized as SG
+import qualified Data.Vector.Sized as SV
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Sized as SU
+import Test.Tasty
+import Test.Tasty.QuickCheck hiding (scale, numTests)
+
+import Quaternion
+import TestUtils
+
+testSuite :: TestTree
+testSuite = testGroup "MultiLaurent"
+  [ otherTests
+  , divideByZeroTests
+  , lawsTests
+  , evalTests
+  , derivTests
+  , patternTests
+  , conversionTests
+  ]
+
+lawsTests :: TestTree
+lawsTests = testGroup "Laws"
+  $ semiringTests ++ ringTests ++ numTests ++ gcdDomainTests ++ isListTests ++ showTests
+
+semiringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+semiringTests =
+  [ mySemiringLaws (Proxy :: Proxy (UMultiLaurent 3 ()))
+  , mySemiringLaws (Proxy :: Proxy (ShortPoly (UMultiLaurent 2 Int8)))
+  , mySemiringLaws (Proxy :: Proxy (ShortPoly (VMultiLaurent 2 Integer)))
+  , tenTimesLess
+  $ mySemiringLaws (Proxy :: Proxy (ShortPoly (UMultiLaurent 2 (Quaternion Int))))
+  ]
+#else
+semiringTests = []
+#endif
+
+ringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+ringTests =
+  [ myRingLaws (Proxy :: Proxy (UMultiLaurent 3 ()))
+  , myRingLaws (Proxy :: Proxy (UMultiLaurent 3 Int8))
+  , myRingLaws (Proxy :: Proxy (VMultiLaurent 3 Integer))
+  , myRingLaws (Proxy :: Proxy (UMultiLaurent 3 (Quaternion Int)))
+  ]
+#else
+ringTests = []
+#endif
+
+numTests :: [TestTree]
+numTests =
+  [ myNumLaws (Proxy :: Proxy (ShortPoly (UMultiLaurent 2 Int8)))
+  , myNumLaws (Proxy :: Proxy (ShortPoly (VMultiLaurent 2 Integer)))
+  , tenTimesLess
+  $ myNumLaws (Proxy :: Proxy (ShortPoly (UMultiLaurent 2 (Quaternion Int))))
+  ]
+
+gcdDomainTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+gcdDomainTests =
+  [ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VMultiLaurent 3 Integer)))
+  , tenTimesLess
+  $ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VMultiLaurent 3 Rational)))
+  ]
+#else
+gcdDomainTests = []
+#endif
+
+isListTests :: [TestTree]
+isListTests =
+  [ myIsListLaws (Proxy :: Proxy (UMultiLaurent 3 ()))
+  , myIsListLaws (Proxy :: Proxy (UMultiLaurent 3 Int8))
+  , myIsListLaws (Proxy :: Proxy (VMultiLaurent 3 Integer))
+  , tenTimesLess
+  $ myIsListLaws (Proxy :: Proxy (UMultiLaurent 3 (Quaternion Int)))
+  ]
+
+showTests :: [TestTree]
+showTests =
+  [ myShowLaws (Proxy :: Proxy (UMultiLaurent 4 ()))
+  , myShowLaws (Proxy :: Proxy (UMultiLaurent 4 Int8))
+  , myShowLaws (Proxy :: Proxy (VMultiLaurent 4 Integer))
+  , tenTimesLess
+  $ myShowLaws (Proxy :: Proxy (UMultiLaurent 4 (Quaternion Int)))
+  ]
+
+otherTests :: TestTree
+otherTests = testGroup "other" $ concat
+  [ otherTestGroup (Proxy :: Proxy Int8)
+  , otherTestGroup (Proxy :: Proxy (Quaternion Int))
+  ]
+
+otherTestGroup
+  :: forall a.
+     (Eq a, Show a, Semiring a, Num a, Arbitrary a, U.Unbox a, G.Vector U.Vector a)
+  => Proxy a
+  -> [TestTree]
+otherTestGroup _ =
+  [ testProperty "scale matches multiplication by monomial" $
+    \p c (xs :: UMultiLaurent 3 a) -> scale p c xs === monomial p c * xs
+  , tenTimesLess $
+    testProperty "toMultiLaurent . unMultiLaurent" $
+    \(xs :: UMultiLaurent 3 a) -> uncurry toMultiLaurent (unMultiLaurent xs) === xs
+  ]
+
+divideByZeroTests :: TestTree
+divideByZeroTests = testGroup "divideByZero"
+  [ testProperty "divide"  $ testProp divide
+  ]
+  where
+    testProp f xs = ioProperty ((== Left DivideByZero) <$> try (evaluate (xs `f` (0 :: VMultiLaurent 3 Rational))))
+
+evalTests :: TestTree
+evalTests = testGroup "eval" $ concat
+  [ evalTestGroup  (Proxy :: Proxy (VMultiLaurent 3 Rational))
+  , substTestGroup (Proxy :: Proxy (UMultiLaurent 3 Int8))
+  ]
+
+evalTestGroup
+  :: forall v a.
+     (Eq a, Field a, Arbitrary a, Show a, Eq (v (Word, a)), Show (v (Word, a)), G.Vector v (Word, a), Eq (v (SU.Vector 3 Word, a)), Show (v (SU.Vector 3 Word, a)), G.Vector v (SU.Vector 3 Word, a))
+  => Proxy (MultiLaurent v 3 a)
+  -> [TestTree]
+evalTestGroup _ =
+  [ testProperty "eval (p + q) r = eval p r + eval q r" $
+    \(ShortPoly p) (ShortPoly q) r -> e (p `plus` q) r === e p r `plus` e q r
+  , testProperty "eval (p * q) r = eval p r * eval q r" $
+    \(ShortPoly p) (ShortPoly q) r -> e (p `times` q) r === e p r `times` e q r
+  , testProperty "eval x p = p" $
+    \p -> e X (SV.fromTuple (p, undefined, undefined)) === p
+  , testProperty "eval (monomial 0 c) p = c" $
+    \c p -> e (monomial 0 c) p === c
+  ]
+  where
+    e :: MultiLaurent v 3 a -> SV.Vector 3 a -> a
+    e = eval
+
+substTestGroup
+  :: forall v a.
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (SU.Vector 3 Word, a)), Show (v (Word, a)), G.Vector v (Word, a), G.Vector v (SU.Vector 3 Word, a))
+  => Proxy (MultiLaurent v 3 a)
+  -> [TestTree]
+substTestGroup _ =
+  [ testProperty "subst x p = p" $
+    \p -> e Data.Poly.Multi.X (SV.fromTuple (p, undefined, undefined)) === p
+  , testProperty "subst (monomial 0 c) p = monomial 0 c" $
+    \c p -> e (Data.Poly.Multi.monomial 0 c) p === monomial 0 c
+  ]
+  where
+    e :: Data.Poly.Multi.MultiPoly v 3 a -> SV.Vector 3 (MultiLaurent v 3 a) -> MultiLaurent v 3 a
+    e = subst
+
+derivTests :: TestTree
+derivTests = testGroup "deriv"
+  [ testProperty "deriv c = 0" $
+    \k c -> deriv k (monomial 0 c :: UMultiLaurent 3 Int) === 0
+  , testProperty "deriv cX = c" $
+    \c -> deriv 0 (monomial 0 c * X :: UMultiLaurent 3 Int) === monomial 0 c
+  , testProperty "deriv (p + q) = deriv p + deriv q" $
+    \k p q -> deriv k (p + q) === (deriv k p + deriv k q :: UMultiLaurent 3 Int)
+  , testProperty "deriv (p * q) = p * deriv q + q * deriv p" $
+    \k p q -> deriv k (p * q) === (p * deriv k q + q * deriv k p :: UMultiLaurent 3 Int)
+  ]
+
+patternTests :: TestTree
+patternTests = testGroup "pattern"
+  [ testProperty "X  :: UMultiLaurent Int" $ once $
+    case (monomial 1 1 :: UMultiLaurent 1 Int) of X -> True; _ -> False
+  , testProperty "X  :: UMultiLaurent Int" $ once $
+    (X :: UMultiLaurent 1 Int) === monomial 1 1
+  , testProperty "X :: UMultiLaurent ()" $ once $
+    case (zero :: UMultiLaurent 1 ()) of X -> True; _ -> False
+  , testProperty "X :: UMultiLaurent ()" $ once $
+    (X :: UMultiLaurent 1 ()) === zero
+
+  , testProperty "Y  :: UMultiLaurent Int" $ once $
+    case (monomial (SG.fromTuple (0, 1)) 1 :: UMultiLaurent 2 Int) of Y -> True; _ -> False
+  , testProperty "Y  :: UMultiLaurent Int" $ once $
+    (Y :: UMultiLaurent 2 Int) === monomial (SG.fromTuple (0, 1)) 1
+  , testProperty "Y :: UMultiLaurent ()" $ once $
+    case (zero :: UMultiLaurent 2 ()) of Y -> True; _ -> False
+  , testProperty "Y :: UMultiLaurent ()" $ once $
+    (Y :: UMultiLaurent 2 ()) === zero
+
+  , testProperty "Z  :: UMultiLaurent Int" $ once $
+    case (monomial (SG.fromTuple (0, 0, 1)) 1 :: UMultiLaurent 3 Int) of Z -> True; _ -> False
+  , testProperty "Z  :: UMultiLaurent Int" $ once $
+    (Z :: UMultiLaurent 3 Int) === monomial (SG.fromTuple (0, 0, 1)) 1
+  , testProperty "Z :: UMultiLaurent ()" $ once $
+    case (zero :: UMultiLaurent 3 ()) of Z -> True; _ -> False
+  , testProperty "Z :: UMultiLaurent ()" $ once $
+    (Z :: UMultiLaurent 3 ()) === zero
+
+  , testProperty "X^-k" $
+    \(NonNegative j) k -> ((X^j)^-k :: UMultiLaurent 1 Int) === monomial (SG.singleton (- j * k)) 1
+  , testProperty "Y^-k" $
+    \(NonNegative j) k -> ((Y^j)^-k :: UMultiLaurent 2 Int) === monomial (SG.fromTuple (0, - j * k)) 1
+  , testProperty "Z^-k" $
+    \(NonNegative j) k -> ((Z^j)^-k :: UMultiLaurent 3 Int) === monomial (SG.fromTuple (0, 0, - j * k)) 1
+  , testProperty "^-" $
+    \(p :: UMultiLaurent 3 Int) (NonNegative k) -> ioProperty $ do
+      et <- try (evaluate (p^-k)) :: IO (Either PatternMatchFail (UMultiLaurent 3 Int))
+      pure $ case et of
+        Left{}  -> True
+        Right t -> p^k * t == one
+  ]
+
+conversionTests :: TestTree
+conversionTests = testGroup "conversions"
+  [ testProperty "unsegregate . segregate = id" $
+    \(xs :: UMultiLaurent 3 Int8) -> xs === unsegregate (segregate xs)
+  , testProperty "segregate . unsegregate = id" $
+    \xs -> xs === segregate (unsegregate xs :: UMultiLaurent 3 Int8)
+  ]
diff --git a/test/Orthogonal.hs b/test/Orthogonal.hs
new file mode 100644
--- /dev/null
+++ b/test/Orthogonal.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE OverloadedLists #-}
+
+module Orthogonal
+  ( testSuite
+  ) where
+
+import Test.Tasty
+
+import Data.List (foldl', tails)
+import Data.Poly (VPoly, deriv, eval, integral)
+import Data.Poly.Orthogonal
+import Test.Tasty.QuickCheck
+
+testSuite :: TestTree
+testSuite = testGroup "Orthogonal"
+  [ testGroup "differential equations"
+    [ testProperty "jacobi"      prop_jacobi_de
+    , testProperty "gegenbauer"  prop_gegenbauer_de
+    , testProperty "legendre"    prop_legendre_de
+    , testProperty "chebyshev1"  prop_chebyshev1_de
+    , testProperty "chebyshev2"  prop_chebyshev2_de
+    , testProperty "hermitePhys" prop_hermitePhys_de
+    , testProperty "laguerre"    prop_laguerre_de
+    , testProperty "laguerreGen" prop_laguerreGen_de
+    ]
+  , testGroup "normalization"
+    [ testProperty "jacobi"     prop_jacobi_norm
+    , testProperty "gegenbauer" prop_gegenbauer_norm
+    , testProperty "legendre"   prop_legendre_norm
+    , testProperty "chebyshev1" prop_chebyshev1_norm
+    , testProperty "chebyshev2" prop_chebyshev2_norm
+    ]
+  , testGroup "orthogonality"
+    [ testProperty "legendre"   prop_legendre_orth
+    ]
+  , testGroup "Hermite"
+    [ testProperty "hermiteProb" prop_hermiteProb
+    , testProperty "hermitePhys" prop_hermitePhys
+    ]
+  ]
+
+prop_jacobi_de :: Rational -> Rational -> Property
+prop_jacobi_de a b = foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] (jacobi a b)
+  where
+    de :: Rational -> VPoly Rational -> VPoly Rational
+    de n y = [1, 0, -1] * deriv (deriv y)
+           + [b - a, - (a + b + 2)] * deriv y
+           + [n * (n + a + b + 1)] * y
+
+prop_gegenbauer_de :: Rational -> Property
+prop_gegenbauer_de g = foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] (gegenbauer g)
+  where
+    de :: Rational -> VPoly Rational -> VPoly Rational
+    de n y = [1, 0, -1] * deriv (deriv y)
+           + [0, - (2 * g + 1)] * deriv y
+           + [n * (n + 2 * g)] * y
+
+prop_legendre_de :: Property
+prop_legendre_de = once $ foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] legendre
+  where
+    de :: Rational -> VPoly Rational -> VPoly Rational
+    de n y = deriv ([1, 0, -1] * deriv y) + [n * (n + 1)] * y
+
+prop_chebyshev1_de :: Property
+prop_chebyshev1_de = once $ foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] chebyshev1
+  where
+    de :: Integer -> VPoly Integer -> VPoly Integer
+    de n y = [1, 0, -1] * deriv (deriv y) + [0, -1] * deriv y + [n * n] * y
+
+prop_chebyshev2_de :: Property
+prop_chebyshev2_de = once $ foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] chebyshev2
+  where
+    de :: Integer -> VPoly Integer -> VPoly Integer
+    de n y = [1, 0, -1] * deriv (deriv y) + [0, -3] * deriv y + [n * (n + 2)] * y
+
+prop_hermitePhys_de :: Property
+prop_hermitePhys_de = once $ foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] hermitePhys
+  where
+    de :: Integer -> VPoly Integer -> VPoly Integer
+    de n y = deriv (deriv y) + [0, -2] * deriv y + [2 * n] * y
+
+prop_laguerre_de :: Property
+prop_laguerre_de = once $ foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] laguerre
+  where
+    de :: Rational -> VPoly Rational -> VPoly Rational
+    de n y = [0, 1] * deriv (deriv y) + [1, -1] * deriv y + [n] * y
+
+prop_laguerreGen_de :: Rational -> Property
+prop_laguerreGen_de a  = foldl' (.&&.) (property True) $
+  zipWith (((=== 0) .) . de) [0..limit] (laguerreGen a)
+  where
+    de :: Rational -> VPoly Rational -> VPoly Rational
+    de n y = [0, 1] * deriv (deriv y) + [1 + a, -1] * deriv y + [n] * y
+
+prop_jacobi_norm :: Rational -> Rational -> Property
+prop_jacobi_norm a b = foldl' (.&&.) (property True) $
+  zipWith (\n y -> norm n === eval y 1) [0..limit] (jacobi a b :: [VPoly Rational])
+  where
+    prod n x = product $ take n $ iterate (subtract 1) (fromIntegral n + x)
+    norm n = prod n a / prod n 0
+
+prop_gegenbauer_norm :: Rational -> Property
+prop_gegenbauer_norm a = foldl' (.&&.) (property True) $
+  zipWith (\n y -> norm n === eval y 1) [0..limit] (gegenbauer a :: [VPoly Rational])
+  where
+    prod n x = product $ take n $ iterate (subtract 1) (fromIntegral n + x)
+    norm n = prod n (a - 1 / 2) / prod n 0
+
+prop_legendre_norm :: Property
+prop_legendre_norm = once $ foldl' (.&&.) (property True) $
+  map ((=== 1) . flip eval 1) (take limit legendre :: [VPoly Rational])
+
+prop_chebyshev1_norm :: Property
+prop_chebyshev1_norm = once $ foldl' (.&&.) (property True) $
+  map ((=== 1) . flip eval 1) (take limit chebyshev1 :: [VPoly Integer])
+
+prop_chebyshev2_norm :: Property
+prop_chebyshev2_norm = once $ foldl' (.&&.) (property True) $
+  zipWith (\n y -> n + 1 === eval y 1) [0..limit] (chebyshev2 :: [VPoly Integer])
+
+prop_legendre_orth :: Property
+prop_legendre_orth = once $ foldl' (.&&.) (property True) $
+  [ integral11 (x * y) === 0 | (x : xs) <- tails polys, y <- xs ]
+  where
+    polys :: [VPoly Rational]
+    polys = take limit legendre
+
+hermiteProbRef :: [VPoly Integer]
+hermiteProbRef = iterate (\he -> [0, 1] * he - deriv he) 1
+
+hermitePhysRef :: [VPoly Integer]
+hermitePhysRef = iterate (\h -> [0, 2] * h - deriv h) 1
+
+prop_hermiteProb :: Property
+prop_hermiteProb = once $ foldl' (.&&.) (property True) $
+  take limit $ zipWith (===) hermiteProb hermiteProbRef
+
+prop_hermitePhys :: Property
+prop_hermitePhys = once $ foldl' (.&&.) (property True) $
+  take limit $ zipWith (===) hermitePhys hermitePhysRef
+
+integral11 :: VPoly Rational -> Rational
+integral11 x = eval y 1 - eval y (-1)
+  where
+    y = integral x
+
+limit :: Num a => a
+limit = 10
diff --git a/test/Quaternion.hs b/test/Quaternion.hs
--- a/test/Quaternion.hs
+++ b/test/Quaternion.hs
@@ -18,18 +18,16 @@
   ) where
 
 import Prelude hiding (negate)
-import Control.Monad
 import Data.Semiring (Semiring(..), Ring(..), minus)
 import GHC.Generics
 import Test.Tasty.QuickCheck hiding (scale)
 
-import Data.Vector.Unboxed (Vector)
+import Data.Vector.Unboxed (Vector, Unbox)
 import qualified Data.Vector.Generic as G
 import Data.Vector.Unboxed.Mutable (MVector)
 import qualified Data.Vector.Generic.Mutable as M
-import Data.Vector.Unboxed (Unbox)
 
-data Quaternion a = Quaternion a a a a
+data Quaternion a = Quaternion !a !a !a !a
   deriving (Eq, Ord, Show, Generic)
 
 instance Ring a => Semiring (Quaternion a) where
@@ -63,9 +61,9 @@
 newtype instance MVector s (Quaternion a) = MV_Quaternion (MVector s (a, a, a, a))
 newtype instance Vector    (Quaternion a) = V_Quaternion  (Vector    (a, a, a, a))
 
-instance (Unbox a) => Unbox (Quaternion a)
+instance Unbox a => Unbox (Quaternion a)
 
-instance (Unbox a) => M.MVector MVector (Quaternion a) where
+instance Unbox a => M.MVector MVector (Quaternion a) where
   {-# INLINE basicLength #-}
   {-# INLINE basicUnsafeSlice #-}
   {-# INLINE basicOverlaps #-}
@@ -81,30 +79,30 @@
   basicLength (MV_Quaternion v) = M.basicLength v
   basicUnsafeSlice i n (MV_Quaternion v) = MV_Quaternion $ M.basicUnsafeSlice i n v
   basicOverlaps (MV_Quaternion v1) (MV_Quaternion v2) = M.basicOverlaps v1 v2
-  basicUnsafeNew n = MV_Quaternion `liftM` M.basicUnsafeNew n
+  basicUnsafeNew n = MV_Quaternion `fmap` M.basicUnsafeNew n
   basicInitialize (MV_Quaternion v) = M.basicInitialize v
-  basicUnsafeReplicate n (Quaternion a b c d) = MV_Quaternion `liftM` M.basicUnsafeReplicate n (a, b, c, d)
-  basicUnsafeRead (MV_Quaternion v) i = (\(a, b, c, d) -> Quaternion a b c d) `liftM` M.basicUnsafeRead v i
+  basicUnsafeReplicate n (Quaternion a b c d) = MV_Quaternion `fmap` M.basicUnsafeReplicate n (a, b, c, d)
+  basicUnsafeRead (MV_Quaternion v) i = (\(a, b, c, d) -> Quaternion a b c d) `fmap` M.basicUnsafeRead v i
   basicUnsafeWrite (MV_Quaternion v) i (Quaternion a b c d) = M.basicUnsafeWrite v i (a, b, c, d)
   basicClear (MV_Quaternion v) = M.basicClear v
   basicSet (MV_Quaternion v) (Quaternion a b c d) = M.basicSet v (a, b, c, d)
   basicUnsafeCopy (MV_Quaternion v1) (MV_Quaternion v2) = M.basicUnsafeCopy v1 v2
   basicUnsafeMove (MV_Quaternion v1) (MV_Quaternion v2) = M.basicUnsafeMove v1 v2
-  basicUnsafeGrow (MV_Quaternion v) n = MV_Quaternion `liftM` M.basicUnsafeGrow v n
+  basicUnsafeGrow (MV_Quaternion v) n = MV_Quaternion `fmap` M.basicUnsafeGrow v n
 
-instance (Unbox a) => G.Vector Vector (Quaternion a) where
+instance Unbox a => G.Vector Vector (Quaternion a) where
   {-# INLINE basicUnsafeFreeze #-}
   {-# INLINE basicUnsafeThaw #-}
   {-# INLINE basicLength #-}
   {-# INLINE basicUnsafeSlice #-}
   {-# INLINE basicUnsafeIndexM #-}
   {-# INLINE elemseq #-}
-  basicUnsafeFreeze (MV_Quaternion v) = V_Quaternion `liftM` G.basicUnsafeFreeze v
-  basicUnsafeThaw (V_Quaternion v) = MV_Quaternion `liftM` G.basicUnsafeThaw v
+  basicUnsafeFreeze (MV_Quaternion v) = V_Quaternion `fmap` G.basicUnsafeFreeze v
+  basicUnsafeThaw (V_Quaternion v) = MV_Quaternion `fmap` G.basicUnsafeThaw v
   basicLength (V_Quaternion v) = G.basicLength v
   basicUnsafeSlice i n (V_Quaternion v) = V_Quaternion $ G.basicUnsafeSlice i n v
   basicUnsafeIndexM (V_Quaternion v) i
-                = (\(a, b, c, d) -> Quaternion a b c d) `liftM` G.basicUnsafeIndexM v i
+                = (\(a, b, c, d) -> Quaternion a b c d) `fmap` G.basicUnsafeIndexM v i
   basicUnsafeCopy (MV_Quaternion mv) (V_Quaternion v)
                 = G.basicUnsafeCopy mv v
   elemseq _ (Quaternion a b c d) z = G.elemseq (undefined :: Vector a) a
diff --git a/test/Sparse.hs b/test/Sparse.hs
--- a/test/Sparse.hs
+++ b/test/Sparse.hs
@@ -1,151 +1,124 @@
 {-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
 {-# LANGUAGE FlexibleContexts           #-}
 {-# LANGUAGE FlexibleInstances          #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE ScopedTypeVariables        #-}
 {-# LANGUAGE UndecidableInstances       #-}
 
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
 module Sparse
   ( testSuite
+  , ShortPoly(..)
   ) where
 
-import Prelude hiding (gcd, quotRem, rem)
-#if MIN_VERSION_semirings(0,4,2)
+import Prelude hiding (gcd, quotRem, quot, rem)
+import Control.Exception
 import Data.Euclidean (Euclidean(..), GcdDomain(..))
-#endif
 import Data.Function
 import Data.Int
-import Data.List
-import Data.Maybe
+import Data.List (groupBy, sortOn)
+import Data.Mod.Word
 import Data.Poly.Sparse
 import qualified Data.Poly.Sparse.Semiring as S
 import Data.Proxy
-import Data.Semiring (Semiring)
+import Data.Semiring (Semiring(..))
 import qualified Data.Vector as V
 import qualified Data.Vector.Generic as G
 import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Sized as SU
 import Test.Tasty
 import Test.Tasty.QuickCheck hiding (scale, numTests)
-import Test.QuickCheck.Classes
 
 import Quaternion
-
-instance (Eq a, Semiring a, Arbitrary a, G.Vector v (Word, a)) => Arbitrary (Poly v a) where
-  arbitrary = S.toPoly . G.fromList <$> arbitrary
-  shrink = fmap (S.toPoly . G.fromList) . shrink . G.toList . unPoly
-
-newtype ShortPoly a = ShortPoly { unShortPoly :: a }
-  deriving
-    ( Eq
-    , Show
-    , Semiring
-#if MIN_VERSION_semirings(0,4,2)
-    , GcdDomain
-    , Euclidean
-#endif
-    )
-
-instance (Eq a, Semiring a, Arbitrary a, G.Vector v (Word, a)) => Arbitrary (ShortPoly (Poly v a)) where
-  arbitrary = ShortPoly . S.toPoly . G.fromList . (\xs -> take (length xs `mod` 5) xs) <$> arbitrary
-  shrink = fmap (ShortPoly . S.toPoly . G.fromList) . shrink . G.toList . unPoly . unShortPoly
+import TestUtils
 
 testSuite :: TestTree
 testSuite = testGroup "Sparse"
-    [ arithmeticTests
-    , otherTests
-    , lawsTests
-    , evalTests
-    , derivTests
-#if MIN_VERSION_semirings(0,4,2)
-    , gcdExtTests
-#endif
-    ]
+  [ arithmeticTests
+  , otherTests
+  , divideByZeroTests
+  , lawsTests
+  , evalTests
+  , derivTests
+  , patternTests
+  , conversionTests
+  ]
 
 lawsTests :: TestTree
 lawsTests = testGroup "Laws"
-  [ semiringTests
-  , ringTests
-  , numTests
-  , euclideanTests
-  , isListTests
-  , showTests
-  ]
+  $ semiringTests ++ ringTests ++ numTests ++ euclideanTests ++ gcdDomainTests ++ isListTests ++ showTests
 
-semiringTests :: TestTree
-semiringTests
-  = testGroup "Semiring"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [ semiringLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , semiringLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , semiringLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , semiringLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+semiringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+semiringTests =
+  [ mySemiringLaws (Proxy :: Proxy (UPoly ()))
+  , mySemiringLaws (Proxy :: Proxy (UPoly Int8))
+  , mySemiringLaws (Proxy :: Proxy (VPoly Integer))
+  , tenTimesLess
+  $ mySemiringLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
-
-ringTests :: TestTree
-ringTests
-  = testGroup "Ring"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_quickcheck_classes(0,6,1)
-    ringLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , ringLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , ringLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , ringLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+#else
+semiringTests = []
 #endif
-  ]
 
-numTests :: TestTree
-numTests
-  = testGroup "Num"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_quickcheck_classes(0,6,3)
-    numLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , numLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , numLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+ringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+ringTests =
+  [ myRingLaws (Proxy :: Proxy (UPoly ()))
+  , myRingLaws (Proxy :: Proxy (UPoly Int8))
+  , myRingLaws (Proxy :: Proxy (VPoly Integer))
+  , myRingLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
+  ]
+#else
+ringTests = []
 #endif
+
+numTests :: [TestTree]
+numTests =
+  [ myNumLaws (Proxy :: Proxy (UPoly Int8))
+  , myNumLaws (Proxy :: Proxy (VPoly Integer))
+  , tenTimesLess
+  $ myNumLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
 
-euclideanTests :: TestTree
-euclideanTests
-  = testGroup "Euclidean"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_semirings(0,4,2) && MIN_VERSION_quickcheck_classes(0,6,3)
-    gcdDomainLaws (Proxy :: Proxy (ShortPoly (Poly V.Vector Integer)))
-  , euclideanLaws (Proxy :: Proxy (ShortPoly (Poly V.Vector Rational)))
+gcdDomainTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+gcdDomainTests =
+  [ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VPoly Integer)))
+  , tenTimesLess
+  $ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (UPoly (Mod 3))))
+  , tenTimesLess
+  $ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VPoly Rational)))
+  ]
+#else
+gcdDomainTests = []
 #endif
+
+euclideanTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+euclideanTests =
+  [ myEuclideanLaws (Proxy :: Proxy (ShortPoly (UPoly (Mod 3))))
+  , myEuclideanLaws (Proxy :: Proxy (ShortPoly (VPoly Rational)))
   ]
+#else
+euclideanTests = []
+#endif
 
-isListTests :: TestTree
-isListTests
-  = testGroup "IsList"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [ isListLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , isListLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , isListLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , isListLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
+isListTests :: [TestTree]
+isListTests =
+  [ myIsListLaws (Proxy :: Proxy (UPoly ()))
+  , myIsListLaws (Proxy :: Proxy (UPoly Int8))
+  , myIsListLaws (Proxy :: Proxy (VPoly Integer))
+  , tenTimesLess
+  $ myIsListLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
 
-showTests :: TestTree
-showTests
-  = testGroup "Show"
-  $ map (uncurry testProperty)
-  $ concatMap lawsProperties
-  [
-#if MIN_VERSION_quickcheck_classes(0,6,0)
-    showLaws (Proxy :: Proxy (Poly U.Vector ()))
-  , showLaws (Proxy :: Proxy (Poly U.Vector Int8))
-  , showLaws (Proxy :: Proxy (Poly V.Vector Integer))
-  , showLaws (Proxy :: Proxy (Poly U.Vector (Quaternion Int)))
-#endif
+showTests :: [TestTree]
+showTests =
+  [ myShowLaws (Proxy :: Proxy (UPoly ()))
+  , myShowLaws (Proxy :: Proxy (UPoly Int8))
+  , myShowLaws (Proxy :: Proxy (VPoly Integer))
+  , tenTimesLess
+  $ myShowLaws (Proxy :: Proxy (UPoly (Quaternion Int)))
   ]
 
 arithmeticTests :: TestTree
@@ -156,9 +129,13 @@
   , testProperty "subtraction matches reference" $
     \(xs :: [(Word, Int)]) ys -> toPoly (V.fromList (subRef xs ys)) ===
       toPoly (V.fromList xs) - toPoly (V.fromList ys)
-  , testProperty "multiplication matches reference" $
+  , tenTimesLess $
+    testProperty "multiplication matches reference" $
     \(xs :: [(Word, Int)]) ys -> toPoly (V.fromList (mulRef xs ys)) ===
       toPoly (V.fromList xs) * toPoly (V.fromList ys)
+  , tenTimesLess $
+    testProperty "quotRemFractional matches quotRem" $
+    \(xs :: VPoly Rational) ys -> ys /= 0 ==> quotRemFractional xs ys === quotRem xs ys
   ]
 
 addRef :: Num a => [(Word, a)] -> [(Word, a)] -> [(Word, a)]
@@ -204,39 +181,54 @@
     \p c -> c /= 0 ==> leading (monomial p c :: UPoly a) === Just (p, c)
   , testProperty "monomial matches reference" $
     \p (c :: a) -> monomial p c === toPoly (V.fromList (monomialRef p c))
-  , testProperty "scale matches multiplication by monomial" $
+  , tenTimesLess $
+    testProperty "scale matches multiplication by monomial" $
     \p c (xs :: UPoly a) -> scale p c xs === monomial p c * xs
+  , tenTimesLess $
+    testProperty "scale' matches multiplication by monomial'" $
+    \p c (xs :: UPoly a) -> S.scale p c xs === S.monomial p c * xs
   ]
 
 monomialRef :: Num a => Word -> a -> [(Word, a)]
 monomialRef p c = [(p, c)]
 
+divideByZeroTests :: TestTree
+divideByZeroTests = testGroup "divideByZero"
+  [ testProperty "quotRem" $ testProp ((uncurry (+) .) . quotRem)
+  , testProperty "quot"    $ testProp quot
+  , testProperty "rem"     $ testProp rem
+  , testProperty "divide"  $ testProp divide
+  , testProperty "degree"  $ once $ degree (0 :: VPoly Rational) === 0
+  ]
+  where
+    testProp f xs = ioProperty ((== Left DivideByZero) <$> try (evaluate (xs `f` (0 :: VPoly Rational))))
+
 evalTests :: TestTree
 evalTests = testGroup "eval" $ concat
-  [ evalTestGroup  (Proxy :: Proxy (Poly U.Vector Int8))
-  , evalTestGroup  (Proxy :: Proxy (Poly V.Vector Integer))
-  , substTestGroup (Proxy :: Proxy (Poly U.Vector Int8))
+  [ evalTestGroup  (Proxy :: Proxy (UPoly Int8))
+  , evalTestGroup  (Proxy :: Proxy (VPoly Integer))
+  , substTestGroup (Proxy :: Proxy (UPoly Int8))
   ]
 
 evalTestGroup
   :: forall v a.
-     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (Word, a)), Show (v (Word, a)), G.Vector v (Word, a))
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (Word, a)), Show (v (Word, a)), G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
   => Proxy (Poly v a)
   -> [TestTree]
 evalTestGroup _ =
   [ testProperty "eval (p + q) r = eval p r + eval q r" $
-    \p q r -> e (p + q) r === e p r + e q r
+    \(ShortPoly p) (ShortPoly q) r -> e (p + q) r === e p r + e q r
   , testProperty "eval (p * q) r = eval p r * eval q r" $
-    \p q r -> e (p * q) r === e p r * e q r
+    \(ShortPoly p) (ShortPoly q) r -> e (p * q) r === e p r * e q r
   , testProperty "eval x p = p" $
     \p -> e X p === p
   , testProperty "eval (monomial 0 c) p = c" $
     \c p -> e (monomial 0 c) p === c
 
   , testProperty "eval' (p + q) r = eval' p r + eval' q r" $
-    \p q r -> e' (p + q) r === e' p r + e' q r
+    \(ShortPoly p) (ShortPoly q) r -> e' (p + q) r === e' p r + e' q r
   , testProperty "eval' (p * q) r = eval' p r * eval' q r" $
-    \p q r -> e' (p * q) r === e' p r * e' q r
+    \(ShortPoly p) (ShortPoly q) r -> e' (p * q) r === e' p r * e' q r
   , testProperty "eval' x p = p" $
     \p -> e' S.X p === p
   , testProperty "eval' (S.monomial 0 c) p = c" $
@@ -251,7 +243,7 @@
 
 substTestGroup
   :: forall v a.
-     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (Word, a)), Show (v (Word, a)), G.Vector v (Word, a))
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (SU.Vector 1 Word, a)), Show (v (SU.Vector 1 Word, a)), G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
   => Proxy (Poly v a)
   -> [TestTree]
 substTestGroup _ =
@@ -273,41 +265,45 @@
 derivTests :: TestTree
 derivTests = testGroup "deriv"
   [ testProperty "deriv = S.deriv" $
-    \(p :: Poly V.Vector Integer) -> deriv p === S.deriv p
-#if MIN_VERSION_semirings(0,5,0)
+    \(p :: VPoly Integer) -> deriv p === S.deriv p
   , testProperty "integral = S.integral" $
-    \(p :: Poly V.Vector Rational) -> integral p === S.integral p
+    \(p :: VPoly Rational) -> integral p === S.integral p
   , testProperty "deriv . integral = id" $
-    \(p :: Poly V.Vector Rational) -> deriv (integral p) === p
-#endif
+    \(p :: VPoly Rational) -> deriv (integral p) === p
   , testProperty "deriv c = 0" $
-    \c -> deriv (monomial 0 c :: Poly V.Vector Int) === 0
+    \c -> deriv (monomial 0 c :: UPoly Int) === 0
   , testProperty "deriv cX = c" $
-    \c -> deriv (monomial 0 c * X :: Poly V.Vector Int) === monomial 0 c
+    \c -> deriv (monomial 0 c * X :: UPoly Int) === monomial 0 c
   , testProperty "deriv (p + q) = deriv p + deriv q" $
-    \p q -> deriv (p + q) === (deriv p + deriv q :: Poly V.Vector Int)
+    \p q -> deriv (p + q) === (deriv p + deriv q :: UPoly Int)
   , testProperty "deriv (p * q) = p * deriv q + q * deriv p" $
-    \p q -> deriv (p * q) === (p * deriv q + q * deriv p :: Poly V.Vector Int)
-  -- , testProperty "deriv (subst p q) = deriv q * subst (deriv p) q" $
-  --   \(p :: Poly V.Vector Int) (q :: Poly U.Vector Int) ->
-  --     deriv (subst p q) === deriv q * subst (deriv p) q
+    \p q -> deriv (p * q) === (p * deriv q + q * deriv p :: UPoly Int)
   ]
 
-#if MIN_VERSION_semirings(0,4,2)
-gcdExtTests :: TestTree
-gcdExtTests = localOption (QuickCheckMaxSize 12) $ testGroup "gcdExt"
-  [ testProperty "gcdExt == S.gcdExt" $
-    \(a :: Poly V.Vector Rational) b ->
-      gcdExt a b === S.gcdExt a b
-  , testProperty "g == as (mod b) for gcdExt" $
-    \(a :: Poly V.Vector Rational) b -> b /= 0 ==>
-      uncurry ((. flip rem b) . (===) . flip rem b) ((* a) <$> gcdExt a b)
-  , testProperty "fst . gcdExt == gcd (mod units)" $
-    \(a :: Poly V.Vector Rational) b ->
-      fst (gcdExt a b) `sameUpToUnits` gcd a b
+patternTests :: TestTree
+patternTests = testGroup "pattern"
+  [ testProperty "X  :: UPoly Int" $ once $
+    case (monomial 1 1 :: UPoly Int) of X -> True; _ -> False
+  , testProperty "X  :: UPoly Int" $ once $
+    (X :: UPoly Int) === monomial 1 1
+  , testProperty "X' :: UPoly Int" $ once $
+    case (S.monomial 1 1 :: UPoly Int) of S.X -> True; _ -> False
+  , testProperty "X' :: UPoly Int" $ once $
+    (S.X :: UPoly Int) === S.monomial 1 1
+  , testProperty "X' :: UPoly ()" $ once $
+    case (zero :: UPoly ()) of S.X -> True; _ -> False
+  , testProperty "X' :: UPoly ()" $ once $
+    (S.X :: UPoly ()) === zero
   ]
 
-sameUpToUnits :: (Eq a, GcdDomain a) => a -> a -> Bool
-sameUpToUnits x y = x == y ||
-  isJust (x `divide` y) && isJust (y `divide` x)
-#endif
+conversionTests :: TestTree
+conversionTests = testGroup "conversions"
+  [ testProperty "denseToSparse . sparseToDense = id" $
+    \(xs :: UPoly Int8) -> xs === denseToSparse (sparseToDense xs)
+  , testProperty "denseToSparse' . sparseToDense' = id" $
+    \(xs :: UPoly Int8) -> xs === S.denseToSparse (S.sparseToDense xs)
+  , testProperty "toPoly . unPoly = id" $
+    \(xs :: UPoly Int8) -> xs === toPoly (unPoly xs)
+  , testProperty "S.toPoly . S.unPoly = id" $
+    \(xs :: UPoly Int8) -> xs === S.toPoly (S.unPoly xs)
+  ]
diff --git a/test/SparseLaurent.hs b/test/SparseLaurent.hs
new file mode 100644
--- /dev/null
+++ b/test/SparseLaurent.hs
@@ -0,0 +1,206 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+module SparseLaurent
+  ( testSuite
+  ) where
+
+import Prelude hiding (gcd, quotRem, quot, rem)
+import Control.Exception
+import Data.Euclidean (GcdDomain(..), Field)
+import Data.Int
+import qualified Data.Poly.Sparse
+import Data.Poly.Sparse.Laurent
+import Data.Proxy
+import Data.Semiring (Semiring(..))
+import qualified Data.Vector.Generic as G
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Sized as SU
+import Test.Tasty
+import Test.Tasty.QuickCheck hiding (scale, numTests)
+
+import Quaternion
+import TestUtils
+
+testSuite :: TestTree
+testSuite = testGroup "SparseLaurent"
+  [ otherTests
+  , divideByZeroTests
+  , lawsTests
+  , evalTests
+  , derivTests
+  , patternTests
+  ]
+
+lawsTests :: TestTree
+lawsTests = testGroup "Laws"
+  $ semiringTests ++ ringTests ++ numTests ++ gcdDomainTests ++ isListTests ++ showTests
+
+semiringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+semiringTests =
+  [ mySemiringLaws (Proxy :: Proxy (ULaurent ()))
+  , mySemiringLaws (Proxy :: Proxy (ULaurent Int8))
+  , mySemiringLaws (Proxy :: Proxy (VLaurent Integer))
+  , tenTimesLess
+  $ mySemiringLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+#else
+semiringTests = []
+#endif
+
+ringTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+ringTests =
+  [ myRingLaws (Proxy :: Proxy (ULaurent ()))
+  , myRingLaws (Proxy :: Proxy (ULaurent Int8))
+  , myRingLaws (Proxy :: Proxy (VLaurent Integer))
+  , myRingLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+#else
+ringTests = []
+#endif
+
+numTests :: [TestTree]
+numTests =
+  [ myNumLaws (Proxy :: Proxy (ULaurent Int8))
+  , myNumLaws (Proxy :: Proxy (VLaurent Integer))
+  , tenTimesLess
+  $ myNumLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+
+gcdDomainTests :: [TestTree]
+#ifdef MIN_VERSION_quickcheck_classes
+gcdDomainTests =
+  [ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VLaurent Integer)))
+  , tenTimesLess
+  $ myGcdDomainLaws (Proxy :: Proxy (ShortPoly (VLaurent Rational)))
+  ]
+#else
+gcdDomainTests = []
+#endif
+
+isListTests :: [TestTree]
+isListTests =
+  [ myIsListLaws (Proxy :: Proxy (ULaurent ()))
+  , myIsListLaws (Proxy :: Proxy (ULaurent Int8))
+  , myIsListLaws (Proxy :: Proxy (VLaurent Integer))
+  , tenTimesLess
+  $ myIsListLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+
+showTests :: [TestTree]
+showTests =
+  [ myShowLaws (Proxy :: Proxy (ULaurent ()))
+  , myShowLaws (Proxy :: Proxy (ULaurent Int8))
+  , myShowLaws (Proxy :: Proxy (VLaurent Integer))
+  , tenTimesLess
+  $ myShowLaws (Proxy :: Proxy (ULaurent (Quaternion Int)))
+  ]
+
+otherTests :: TestTree
+otherTests = testGroup "other" $ concat
+  [ otherTestGroup (Proxy :: Proxy Int8)
+  , otherTestGroup (Proxy :: Proxy (Quaternion Int))
+  ]
+
+otherTestGroup
+  :: forall a.
+     (Eq a, Show a, Semiring a, Num a, Arbitrary a, U.Unbox a, G.Vector U.Vector a)
+  => Proxy a
+  -> [TestTree]
+otherTestGroup _ =
+  [ testProperty "leading p 0 == Nothing" $
+    \p -> leading (monomial p 0 :: ULaurent a) === Nothing
+  , testProperty "leading . monomial = id" $
+    \p c -> c /= 0 ==> leading (monomial p c :: ULaurent a) === Just (p, c)
+  , tenTimesLess $
+    testProperty "scale matches multiplication by monomial" $
+    \p c (xs :: ULaurent a) -> scale p c xs === monomial p c * xs
+  , tenTimesLess $
+    testProperty "toLaurent . unLaurent" $
+    \(xs :: ULaurent a) -> uncurry toLaurent (unLaurent xs) === xs
+  ]
+
+divideByZeroTests :: TestTree
+divideByZeroTests = testGroup "divideByZero"
+  [ testProperty "divide"  $ testProp divide
+  ]
+  where
+    testProp f xs = ioProperty ((== Left DivideByZero) <$> try (evaluate (xs `f` (0 :: VLaurent Rational))))
+
+evalTests :: TestTree
+evalTests = testGroup "eval" $ concat
+  [ evalTestGroup  (Proxy :: Proxy (VLaurent Rational))
+  , substTestGroup (Proxy :: Proxy (ULaurent Int8))
+  ]
+
+evalTestGroup
+  :: forall v a.
+     (Eq a, Field a, Arbitrary a, Show a, Eq (v (Word, a)), Show (v (Word, a)), G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
+  => Proxy (Laurent v a)
+  -> [TestTree]
+evalTestGroup _ =
+  [ testProperty "eval (p + q) r = eval p r + eval q r" $
+    \(ShortPoly p) (ShortPoly q) r -> e (p `plus` q) r === e p r `plus` e q r
+  , testProperty "eval (p * q) r = eval p r * eval q r" $
+    \(ShortPoly p) (ShortPoly q) r -> e (p `times` q) r === e p r `times` e q r
+  , testProperty "eval x p = p" $
+    \p -> e X p === p
+  , testProperty "eval (monomial 0 c) p = c" $
+    \c p -> e (monomial 0 c) p === c
+  ]
+  where
+    e :: Laurent v a -> a -> a
+    e = eval
+
+substTestGroup
+  :: forall v a.
+     (Eq a, Num a, Semiring a, Arbitrary a, Show a, Eq (v (SU.Vector 1 Word, a)), Show (v (Word, a)), G.Vector v (Word, a), G.Vector v (SU.Vector 1 Word, a))
+  => Proxy (Laurent v a)
+  -> [TestTree]
+substTestGroup _ =
+  [ testProperty "subst x p = p" $
+    \p -> e Data.Poly.Sparse.X p === p
+  , testProperty "subst (monomial 0 c) p = monomial 0 c" $
+    \c p -> e (Data.Poly.Sparse.monomial 0 c) p === monomial 0 c
+  ]
+  where
+    e :: Data.Poly.Sparse.Poly v a -> Laurent v a -> Laurent v a
+    e = subst
+
+derivTests :: TestTree
+derivTests = testGroup "deriv"
+  [ testProperty "deriv c = 0" $
+    \c -> deriv (monomial 0 c :: ULaurent Int) === 0
+  , testProperty "deriv cX = c" $
+    \c -> deriv (monomial 0 c * X :: ULaurent Int) === monomial 0 c
+  , testProperty "deriv (p + q) = deriv p + deriv q" $
+    \p q -> deriv (p + q) === (deriv p + deriv q :: ULaurent Int)
+  , testProperty "deriv (p * q) = p * deriv q + q * deriv p" $
+    \p q -> deriv (p * q) === (p * deriv q + q * deriv p :: ULaurent Int)
+  ]
+
+patternTests :: TestTree
+patternTests = testGroup "pattern"
+  [ testProperty "X  :: ULaurent Int" $ once $
+    case (monomial 1 1 :: ULaurent Int) of X -> True; _ -> False
+  , testProperty "X  :: ULaurent Int" $ once $
+    (X :: ULaurent Int) === monomial 1 1
+  , testProperty "X :: ULaurent ()" $ once $
+    case (zero :: ULaurent ()) of X -> True; _ -> False
+  , testProperty "X :: ULaurent ()" $ once $
+    (X :: ULaurent ()) === zero
+  , testProperty "X^-k" $
+    \(NonNegative j) k -> ((X^j)^-k :: ULaurent Int) === monomial (- j * k) 1
+  , testProperty "^-" $
+    \(p :: ULaurent Int) (NonNegative k) -> ioProperty $ do
+      et <- try (evaluate (p^-k)) :: IO (Either PatternMatchFail (ULaurent Int))
+      pure $ case et of
+        Left{}  -> True
+        Right t -> p^k * t == one
+  ]
diff --git a/test/TestUtils.hs b/test/TestUtils.hs
new file mode 100644
--- /dev/null
+++ b/test/TestUtils.hs
@@ -0,0 +1,188 @@
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE UndecidableInstances       #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module TestUtils
+  ( ShortPoly(..)
+  , tenTimesLess
+  , myNumLaws
+#ifdef MIN_VERSION_quickcheck_classes
+  , mySemiringLaws
+  , myRingLaws
+  , myGcdDomainLaws
+  , myEuclideanLaws
+#endif
+  , myIsListLaws
+  , myShowLaws
+  ) where
+
+import Prelude hiding (lcm, rem)
+import Data.Euclidean
+import Data.Mod.Word
+import Data.Proxy
+import Data.Semiring (Semiring(..), Ring)
+import qualified Data.Vector.Generic as G
+import GHC.Exts
+import GHC.TypeNats (KnownNat)
+import Test.QuickCheck.Classes.Base
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+#ifdef MIN_VERSION_quickcheck_classes
+import Test.QuickCheck.Classes
+#endif
+
+import qualified Data.Poly.Semiring as Dense
+import qualified Data.Poly.Laurent as DenseLaurent
+
+#ifdef SupportSparse
+import Control.Arrow
+import Data.Finite
+import qualified Data.Vector.Generic.Sized as SG
+import qualified Data.Vector.Unboxed.Sized as SU
+import Data.Poly.Multi.Semiring
+import qualified Data.Poly.Multi.Laurent as MultiLaurent
+#endif
+
+newtype ShortPoly a = ShortPoly { unShortPoly :: a }
+  deriving (Eq, Show, Semiring, GcdDomain, Euclidean, Num)
+
+instance KnownNat m => Arbitrary (Mod m) where
+  arbitrary = oneof [arbitraryBoundedEnum, fromInteger <$> arbitrary]
+  shrink = map fromInteger . shrink . toInteger . unMod
+
+instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (Dense.Poly v a) where
+  arbitrary = Dense.toPoly . G.fromList <$> arbitrary
+  shrink = fmap (Dense.toPoly . G.fromList) . shrink . G.toList . Dense.unPoly
+
+instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (ShortPoly (Dense.Poly v a)) where
+  arbitrary = ShortPoly . Dense.toPoly . G.fromList . (\xs -> take (length xs `mod` 10) xs) <$> arbitrary
+  shrink = fmap (ShortPoly . Dense.toPoly . G.fromList) . shrink . G.toList . Dense.unPoly . unShortPoly
+
+instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (DenseLaurent.Laurent v a) where
+  arbitrary = DenseLaurent.toLaurent <$> ((`rem` 10) <$> arbitrary) <*> arbitrary
+  shrink = fmap (uncurry DenseLaurent.toLaurent) . shrink . DenseLaurent.unLaurent
+
+instance (Eq a, Semiring a, Arbitrary a, G.Vector v a) => Arbitrary (ShortPoly (DenseLaurent.Laurent v a)) where
+  arbitrary = (ShortPoly .) . DenseLaurent.toLaurent <$> ((`rem` 10) <$> arbitrary) <*> (unShortPoly <$> arbitrary)
+  shrink = fmap (ShortPoly . uncurry DenseLaurent.toLaurent . fmap unShortPoly) . shrink . fmap ShortPoly . DenseLaurent.unLaurent . unShortPoly
+
+#ifdef SupportSparse
+
+instance KnownNat n => Arbitrary (Finite n) where
+  arbitrary = elements finites
+
+instance (Arbitrary a, KnownNat n, G.Vector v a) => Arbitrary (SG.Vector v n a) where
+  arbitrary = SG.replicateM arbitrary
+  shrink vs = [ vs SG.// [(i, x)] | i <- finites, x <- shrink (SG.index vs i) ]
+
+instance (Eq a, Semiring a, Arbitrary a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Arbitrary (MultiPoly v n a) where
+  arbitrary = toMultiPoly . G.fromList <$> arbitrary
+  shrink = fmap (toMultiPoly . G.fromList) . shrink . G.toList . unMultiPoly
+
+instance (Eq a, Semiring a, Arbitrary a, KnownNat n, G.Vector v (SU.Vector n Word, a)) => Arbitrary (ShortPoly (MultiPoly v n a)) where
+  arbitrary = ShortPoly . toMultiPoly . G.fromList . (\xs -> take (length xs `mod` 4) (map (first (SU.map (`mod` 3))) xs)) <$> arbitrary
+  shrink = fmap (ShortPoly . toMultiPoly . G.fromList) . shrink . G.toList . unMultiPoly . unShortPoly
+
+instance (Eq a, Semiring a, Arbitrary a, KnownNat n, G.Vector v (Word, a), G.Vector v (SU.Vector n Word, a)) => Arbitrary (MultiLaurent.MultiLaurent v n a) where
+  arbitrary = MultiLaurent.toMultiLaurent <$> (SU.map (`rem` 10) <$> arbitrary) <*> arbitrary
+  shrink = fmap (uncurry MultiLaurent.toMultiLaurent) . shrink . MultiLaurent.unMultiLaurent
+
+instance (Eq a, Semiring a, Arbitrary a, KnownNat n, G.Vector v (Word, a), G.Vector v (SU.Vector n Word, a)) => Arbitrary (ShortPoly (MultiLaurent.MultiLaurent v n a)) where
+  arbitrary = (ShortPoly .) . MultiLaurent.toMultiLaurent <$> (SU.map (`rem` 10) <$> arbitrary) <*> (unShortPoly <$> arbitrary)
+  shrink = fmap (ShortPoly . uncurry MultiLaurent.toMultiLaurent . fmap unShortPoly) . shrink . fmap ShortPoly . MultiLaurent.unMultiLaurent . unShortPoly
+
+#endif
+
+-------------------------------------------------------------------------------
+
+tenTimesLess :: TestTree -> TestTree
+tenTimesLess = adjustOption $
+  \(QuickCheckTests n) -> QuickCheckTests (max 100 (n `div` 10))
+
+myNumLaws :: (Eq a, Num a, Arbitrary a, Show a) => Proxy a -> TestTree
+myNumLaws proxy = testGroup tpclss $ map tune props
+  where
+    Laws tpclss props = numLaws proxy
+
+    tune pair = case fst pair of
+      "Multiplicative Associativity" ->
+        tenTimesLess test
+      "Multiplication Left Distributes Over Addition" ->
+        tenTimesLess test
+      "Multiplication Right Distributes Over Addition" ->
+        tenTimesLess test
+      "Subtraction" ->
+        tenTimesLess test
+      _ -> test
+      where
+        test = uncurry testProperty pair
+
+#ifdef MIN_VERSION_quickcheck_classes
+
+mySemiringLaws :: (Eq a, Semiring a, Arbitrary a, Show a) => Proxy a -> TestTree
+mySemiringLaws proxy = testGroup tpclss $ map tune props
+  where
+    Laws tpclss props = semiringLaws proxy
+
+    tune pair = case fst pair of
+      "Multiplicative Associativity" ->
+        tenTimesLess test
+      "Multiplication Left Distributes Over Addition" ->
+        tenTimesLess test
+      "Multiplication Right Distributes Over Addition" ->
+        tenTimesLess test
+      _ -> test
+      where
+        test = uncurry testProperty pair
+
+myRingLaws :: (Eq a, Ring a, Arbitrary a, Show a) => Proxy a -> TestTree
+myRingLaws proxy = testGroup tpclss $ map (uncurry testProperty) props
+  where
+    Laws tpclss props = ringLaws proxy
+
+myGcdDomainLaws :: forall a. (Eq a, GcdDomain a, Arbitrary a, Show a) => Proxy a -> TestTree
+myGcdDomainLaws proxy = testGroup tpclss $ map tune $ lcm0 : props
+  where
+    Laws tpclss props = gcdDomainLaws proxy
+
+    tune pair = case fst pair of
+      "gcd1"    -> tenTimesLess $ tenTimesLess test
+      "gcd2"    -> tenTimesLess $ tenTimesLess test
+      "lcm1"    -> tenTimesLess $ tenTimesLess $ tenTimesLess test
+      "lcm2"    -> tenTimesLess test
+      "coprime" -> tenTimesLess $ tenTimesLess test
+      _ -> test
+      where
+        test = uncurry testProperty pair
+
+    lcm0 = ("lcm0", property $ \(x :: a) -> lcm x zero === zero .&&. lcm zero x === zero)
+
+myEuclideanLaws :: (Eq a, Euclidean a, Arbitrary a, Show a) => Proxy a -> TestTree
+myEuclideanLaws proxy = testGroup tpclss $ map (uncurry testProperty) props
+  where
+    Laws tpclss props = euclideanLaws proxy
+
+#endif
+
+myIsListLaws :: (Eq a, IsList a, Arbitrary a, Show a, Show (Item a), Arbitrary (Item a)) => Proxy a -> TestTree
+myIsListLaws proxy = testGroup tpclss $ map (uncurry testProperty) props
+  where
+    Laws tpclss props = isListLaws proxy
+
+myShowLaws :: (Eq a, Arbitrary a, Show a) => Proxy a -> TestTree
+myShowLaws proxy = testGroup tpclss $ map tune props
+  where
+    Laws tpclss props = showLaws proxy
+
+    tune pair = case fst pair of
+      "Equivariance: showList" -> tenTimesLess $ tenTimesLess test
+      _ -> test
+      where
+        test = uncurry testProperty pair
