diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2018, Metrix.AI
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/edges.cabal b/edges.cabal
new file mode 100644
--- /dev/null
+++ b/edges.cabal
@@ -0,0 +1,93 @@
+name:
+  edges
+version:
+  0.4
+category:
+  Graphs
+synopsis:
+  Tools for efficient immutable graphs
+description:
+  A set of tools for constructing immutable graphs which are both memory and performance-efficient.
+homepage:
+  https://github.com/metrix-ai/edges
+bug-reports:
+  https://github.com/metrix-ai/edges/issues
+author:
+  Nikita Volkov <nikita.y.volkov@mail.ru>
+maintainer:
+  Metrix Ninjas <ninjas@metrix.ai>
+copyright:
+  (c) 2018, Metrix.AI
+license:
+  MIT
+license-file:
+  LICENSE
+build-type:
+  Simple
+cabal-version:
+  >=1.10
+
+source-repository head
+  type:
+    git
+  location:
+    git://github.com/metrix-ai/edges.git
+
+library
+  hs-source-dirs:
+    library
+  default-extensions:
+    Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, PatternSynonyms, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedTuples, OverloadedLists
+  default-language:
+    Haskell2010
+  exposed-modules:
+    Edges.Edges
+    Edges.NodeCounts
+    Edges.Node
+    Edges.Potoki.Produce
+    Edges.IO
+  other-modules:
+    Edges.Cereal.Instances
+    Edges.Cereal.Get
+    Edges.Cereal.Put
+    Edges.Prelude
+    Edges.Types
+  build-depends:
+    base >=4.7 && <5,
+    cereal >=0.5.5 && <0.6,
+    contravariant >=1.4 && <2,
+    deferred-folds >=0.6 && <0.7,
+    foldl >=1.4.2 && <2,
+    monad-par >=0.3.4.8 && <0.4,
+    pointed >=5 && <6,
+    potoki >=2.0.2 && <2.1,
+    potoki-cereal >=0.2.1.1 && <0.3,
+    primitive >=0.6.4 && <0.7,
+    primitive-extras >=0.3 && <0.4,
+    profunctors >=5 && <6,
+    QuickCheck >=2.8.1 && <3,
+    semigroupoids >=5.2 && <6,
+    text >=1 && <2,
+    vector >=0.12 && <0.13
+
+test-suite test
+  type:
+    exitcode-stdio-1.0
+  hs-source-dirs:
+    test
+  main-is:
+    Main.hs
+  default-extensions:
+    Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, PatternSynonyms, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedTuples, OverloadedLists
+  default-language:
+    Haskell2010
+  build-depends:
+    cereal,
+    edges,
+    foldl,
+    QuickCheck >=2.8.1 && <3,
+    quickcheck-instances >=0.3.11 && <0.4,
+    rerebase >=1.1 && <2,
+    tasty >=1.0.1 && <1.2,
+    tasty-hunit >=0.10 && <0.11,
+    tasty-quickcheck >=0.10 && <0.11
diff --git a/library/Edges/Cereal/Get.hs b/library/Edges/Cereal/Get.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Cereal/Get.hs
@@ -0,0 +1,22 @@
+module Edges.Cereal.Get
+where
+
+import Edges.Prelude
+import Edges.Types
+import Data.Serialize.Get
+import PrimitiveExtras.Cereal.Get
+
+
+nodeCounts :: Get (NodeCounts entity)
+nodeCounts =
+  NodeCounts <$> primArray getWord32le
+
+edges :: Get (Edges a b)
+edges =
+  do
+    targetSpace <- fromIntegral <$> getInt64le
+    pma <- primMultiArray getWord32le
+    return (Edges targetSpace pma)
+
+node :: Get (Node a)
+node = Node . fromIntegral <$> getInt64le
diff --git a/library/Edges/Cereal/Instances.hs b/library/Edges/Cereal/Instances.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Cereal/Instances.hs
@@ -0,0 +1,21 @@
+module Edges.Cereal.Instances
+where
+
+import Edges.Prelude
+import Edges.Types
+import Data.Serialize
+import qualified Edges.Cereal.Get as A
+import qualified Edges.Cereal.Put as B
+
+
+instance Serialize (Node a) where
+  get = A.node
+  put = B.node
+
+instance Serialize (NodeCounts a) where
+  get = A.nodeCounts
+  put = B.nodeCounts
+
+instance Serialize (Edges a b) where
+  get = A.edges
+  put = B.edges
diff --git a/library/Edges/Cereal/Put.hs b/library/Edges/Cereal/Put.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Cereal/Put.hs
@@ -0,0 +1,22 @@
+module Edges.Cereal.Put
+where
+
+import Edges.Prelude
+import Edges.Types
+import Data.Serialize.Put
+import PrimitiveExtras.Cereal.Put
+
+
+nodeCounts :: Putter (NodeCounts entity)
+nodeCounts (NodeCounts pa) =
+  primArray putWord32le pa
+
+edges :: Putter (Edges a b)
+edges (Edges targetSpaceValue mpaValue) =
+  targetSpace <> mpa
+  where
+    targetSpace = putInt64le (fromIntegral targetSpaceValue)
+    mpa = primMultiArray putWord32le mpaValue
+
+node :: Putter (Node a)
+node (Node int) = putInt64le (fromIntegral int)
diff --git a/library/Edges/Edges.hs b/library/Edges/Edges.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Edges.hs
@@ -0,0 +1,80 @@
+module Edges.Edges
+(
+  Edges,
+  list,
+  listBipartite,
+  primListBipartite,
+  toAssocUnfoldM,
+  toAssocList,
+  genBipartiteWithLimits,
+)
+where
+
+import Edges.Prelude
+import Edges.Types
+import Edges.Cereal.Instances ()
+import qualified PrimitiveExtras.Monad as Monad
+import qualified Control.Foldl as Foldl
+import qualified Control.Monad.Par as Par
+import qualified PrimitiveExtras.UnfoldM as UnfoldM
+import qualified DeferredFolds.UnfoldM as UnfoldM
+import qualified Test.QuickCheck.Gen as Gen
+
+deriving instance Eq (Edges a b)
+
+deriving instance Show (Edges a b)
+
+list :: [(Node a, Node b)] -> Edges a b
+list list =
+  Par.runPar $ do
+    aSizeFuture <- Par.spawnP $ succ $ fromMaybe 0 $ flip Foldl.fold list $ flip lmap Foldl.maximum $ \ (Node x, _) -> x
+    bSizeFuture <- Par.spawnP $ succ $ fromMaybe 0 $ flip Foldl.fold list $ flip lmap Foldl.maximum $ \ (_, Node x) -> x
+    aToBPrimFoldableFuture <- Par.spawnP $ flip fmap list $ \ (Node aInt, Node bInt) -> (aInt, fromIntegral bInt)
+    aSize <- Par.get aSizeFuture
+    bSize <- Par.get bSizeFuture
+    aToBEdges <- primFoldableWithAmounts aSize bSize <$> Par.get aToBPrimFoldableFuture
+    return aToBEdges
+
+listBipartite :: [(Node a, Node b)] -> (Edges a b, Edges b a)
+listBipartite = coerce primListBipartite
+
+primListBipartite :: [(Int, Int)] -> (Edges a b, Edges b a)
+primListBipartite list =
+  Par.runPar $ do
+    aSizeFuture <- Par.spawnP $ succ $ fromMaybe 0 $ flip Foldl.fold list $ flip lmap Foldl.maximum fst
+    bSizeFuture <- Par.spawnP $ succ $ fromMaybe 0 $ flip Foldl.fold list $ flip lmap Foldl.maximum snd
+    aToBPrimFoldableFuture <- Par.spawnP $ flip fmap list $ \ (aInt, bInt) -> (aInt, fromIntegral bInt)
+    bToAPrimFoldableFuture <- Par.spawnP $ flip fmap list $ \ (aInt, bInt) -> (bInt, fromIntegral aInt)
+    aSize <- Par.get aSizeFuture
+    bSize <- Par.get bSizeFuture
+    aToBEdgesFuture <- Par.spawn_ $ primFoldableWithAmounts aSize bSize <$> Par.get aToBPrimFoldableFuture
+    bToAEdgesFuture <- Par.spawn_ $ primFoldableWithAmounts bSize aSize <$> Par.get bToAPrimFoldableFuture
+    aToBEdges <- Par.get aToBEdgesFuture
+    bToAEdges <- Par.get bToAEdgesFuture
+    return (aToBEdges, bToAEdges)
+
+primFoldableWithAmounts :: Foldable f => Int -> Int -> f (Int, Word32) -> Edges a b
+primFoldableWithAmounts aAmount bAmount foldable =
+  Edges bAmount $ runIdentity $ Monad.primMultiArray aAmount $ \ fold ->
+  Identity $ Foldl.fold fold foldable
+
+toAssocUnfoldM :: Monad m => Edges a b -> UnfoldM m (Node a, Node b)
+toAssocUnfoldM (Edges _ mpa) =
+  fmap (\ (aInt, bWord32) -> (Node aInt, Node (fromIntegral bWord32))) $
+  UnfoldM.primMultiArrayAssocs mpa
+
+toAssocList :: Edges a b -> [(Node a, Node b)]
+toAssocList edges =
+  UnfoldM.fold Foldl.list (toAssocUnfoldM edges)
+
+genBipartiteWithLimits :: Int -> Int -> Gen.Gen (Edges a b, Edges b a)
+genBipartiteWithLimits nodeLimit edgeLimit =
+  do
+    aMaxIndex <- Gen.choose (0, pred nodeLimit)
+    bMaxIndex <- Gen.choose (0, pred nodeLimit)
+    edgesAmount <- Gen.choose (0, edgeLimit)
+    if aMaxIndex == 0 || bMaxIndex == 0
+      then return (primListBipartite [])
+      else do
+        edges <- replicateM edgesAmount $ (,) <$> Gen.choose (0, aMaxIndex) <*> Gen.choose (0, bMaxIndex)
+        return (primListBipartite edges)
diff --git a/library/Edges/IO.hs b/library/Edges/IO.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/IO.hs
@@ -0,0 +1,18 @@
+module Edges.IO
+where
+
+import Edges.Prelude
+import Edges.Types
+import Edges.Node ()
+import Edges.NodeCounts ()
+import Potoki.IO
+import qualified Edges.Potoki.Produce as A
+import qualified Potoki.Transform as B
+import qualified Potoki.Cereal.Consume as C
+
+
+encodeNodeCountsToFile :: FilePath -> Edges a x -> (Node a -> NodeCounts z) -> IO (Either IOException ())
+encodeNodeCountsToFile file edges nodeCounts =
+  produceAndConsume
+    (A.nodeCounts edges nodeCounts)
+    (C.encodeToFile file)
diff --git a/library/Edges/Node.hs b/library/Edges/Node.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Node.hs
@@ -0,0 +1,21 @@
+module Edges.Node
+(
+  Node(..),
+  genWithLimit,
+)
+where
+
+import Edges.Prelude
+import Edges.Types
+import Edges.Cereal.Instances ()
+import qualified Test.QuickCheck.Gen as Gen
+
+instance Show (Node a) where
+  show (Node int) = show int
+
+deriving instance Eq (Node a)
+
+deriving instance Ord (Node a)
+
+genWithLimit :: Int -> Gen.Gen (Node a)
+genWithLimit max = Node <$> Gen.choose (0, max)
diff --git a/library/Edges/NodeCounts.hs b/library/Edges/NodeCounts.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/NodeCounts.hs
@@ -0,0 +1,67 @@
+module Edges.NodeCounts
+(
+  NodeCounts,
+  node,
+  nodeTargets,
+  targets,
+  toList,
+  toUnboxedVector,
+)
+where
+
+import Edges.Prelude hiding (index, toList)
+import Edges.Types
+import Edges.Cereal.Instances ()
+import qualified PrimitiveExtras.UnfoldM as A
+import qualified PrimitiveExtras.Pure as C
+import qualified PrimitiveExtras.IO as D
+import qualified PrimitiveExtras.Fold as E
+import qualified DeferredFolds.UnfoldM as B
+import qualified Data.Vector.Unboxed as F
+import qualified Control.Monad.Par.IO as Par
+import qualified Control.Monad.Par as Par hiding (runParIO)
+
+
+instance Show (NodeCounts a) where
+  show = show . toList
+
+node :: Edges entity anyEntity -> Node entity -> NodeCounts entity
+node (Edges _ edgesPma) =
+  let size = C.primMultiArrayOuterLength edgesPma
+      in nodeWithSize size
+
+nodeWithSize :: Int -> Node entity -> NodeCounts entity
+nodeWithSize size (Node index) =
+  NodeCounts (C.oneHotPrimArray size index 1)
+
+nodeTargets :: Edges surce target -> Node source -> NodeCounts target
+nodeTargets (Edges targetAmount edgesPma) (Node sourceIndex) =
+  let indexUnfold = fmap fromIntegral (A.primMultiArrayAt edgesPma sourceIndex)
+      indexFold = E.indexCounts targetAmount
+      countPa = B.fold indexFold indexUnfold
+      in NodeCounts countPa
+
+{-|
+Count the occurrences of targets based on the occurrences of sources.
+
+Utilizes concurrency.
+-}
+targets :: Edges source target -> NodeCounts source -> NodeCounts target
+targets (Edges targetAmount edgesPma) (NodeCounts sourceCountsPa) =
+  unsafePerformIO $ Par.runParIO $ do
+    targetCountVarTable <- liftIO (D.newTVarArray 0 targetAmount)
+    Par.parFor (Par.InclusiveRange 0 (pred (sizeofPrimArray sourceCountsPa))) $ \ sourceIndex ->
+      case indexPrimArray sourceCountsPa sourceIndex of
+        0 -> return ()
+        sourceCount ->
+          liftIO $
+          B.forM_ (A.primMultiArrayAt edgesPma sourceIndex) $ \ targetIndex ->
+          D.modifyTVarArrayAt targetCountVarTable (fromIntegral targetIndex) (+ sourceCount)
+    targetCountsPa <- liftIO (D.freezeTVarArrayAsPrimArray targetCountVarTable)
+    return (NodeCounts targetCountsPa)
+
+toList :: NodeCounts entity -> [Word32]
+toList (NodeCounts pa) = foldrPrimArray' (:) [] pa
+
+toUnboxedVector :: NodeCounts entity -> F.Vector Word32
+toUnboxedVector (NodeCounts pa) = C.primArrayUnboxedVector pa
diff --git a/library/Edges/Potoki/Produce.hs b/library/Edges/Potoki/Produce.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Potoki/Produce.hs
@@ -0,0 +1,25 @@
+module Edges.Potoki.Produce
+where
+
+import Edges.Prelude
+import Edges.Types
+import Edges.NodeCounts ()
+import Potoki.Produce
+import qualified PrimitiveExtras.Pure as A
+import qualified Potoki.Transform as B
+import qualified Potoki.Cereal.Produce as C
+
+
+nodes :: Edges a x -> Produce (Node a)
+nodes (Edges _ pma) =
+  coerce $
+  enumInRange 0 (pred (A.primMultiArrayOuterLength pma))
+
+nodeCounts :: Edges a x -> (Node a -> NodeCounts b) -> Produce (Node a, NodeCounts b)
+nodeCounts edges nodeCounts =
+  transform (B.concurrently numCapabilities (arr (\ node -> case nodeCounts node of x -> (node, x)))) $
+  nodes edges
+
+nodeCountsFromFile :: FilePath -> Produce (Either IOException (Either Text (Node a, NodeCounts b)))
+nodeCountsFromFile file =
+  C.fileDecoded file
diff --git a/library/Edges/Prelude.hs b/library/Edges/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Prelude.hs
@@ -0,0 +1,149 @@
+module Edges.Prelude
+(
+  module Exports,
+  UnboxedVector,
+  modifyTVar',
+  forMToZero_,
+  forMFromZero_,
+  (.),
+)
+where
+
+
+-- base
+-------------------------
+import Control.Applicative as Exports
+import Control.Arrow as Exports
+import Control.Category as Exports hiding ((.))
+import Control.Concurrent as Exports
+import Control.Exception as Exports
+import Control.Monad as Exports hiding (mapM_, sequence_, forM_, msum, mapM, sequence, forM)
+import Control.Monad.IO.Class as Exports
+import Control.Monad.Fix as Exports hiding (fix)
+import Control.Monad.ST as Exports
+import Data.Bits as Exports
+import Data.Bool as Exports
+import Data.Char as Exports
+import Data.Coerce as Exports
+import Data.Complex as Exports
+import Data.Data as Exports
+import Data.Dynamic as Exports
+import Data.Either as Exports
+import Data.Fixed as Exports
+import Data.Foldable as Exports
+import Data.Function as Exports hiding (id, (.))
+import Data.Functor as Exports
+import Data.Functor.Identity as Exports
+import Data.Int as Exports
+import Data.IORef as Exports
+import Data.Ix as Exports
+import Data.List as Exports hiding (sortOn, isSubsequenceOf, uncons, concat, foldr, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, find, maximumBy, minimumBy, mapAccumL, mapAccumR, foldl')
+import Data.Maybe as Exports
+import Data.Monoid as Exports hiding (Last(..), First(..))
+import Data.Ord as Exports
+import Data.Proxy as Exports
+import Data.Ratio as Exports
+import Data.STRef as Exports
+import Data.String as Exports
+import Data.Traversable as Exports
+import Data.Tuple as Exports
+import Data.Unique as Exports
+import Data.Version as Exports
+import Data.Word as Exports
+import Debug.Trace as Exports
+import Foreign.ForeignPtr as Exports
+import Foreign.Ptr as Exports
+import Foreign.StablePtr as Exports
+import Foreign.Storable as Exports hiding (sizeOf, alignment)
+import GHC.Conc as Exports hiding (withMVar, threadWaitWriteSTM, threadWaitWrite, threadWaitReadSTM, threadWaitRead)
+import GHC.Exts as Exports (lazy, inline, sortWith, groupWith)
+import GHC.Generics as Exports (Generic)
+import GHC.IO.Exception as Exports
+import Numeric as Exports
+import Prelude as Exports hiding (concat, foldr, mapM_, sequence_, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, mapM, sequence, id, (.))
+import System.Environment as Exports
+import System.Exit as Exports
+import System.IO as Exports
+import System.IO.Error as Exports
+import System.IO.Unsafe as Exports
+import System.Mem as Exports
+import System.Mem.StableName as Exports
+import System.Timeout as Exports
+import Text.ParserCombinators.ReadP as Exports (ReadP, ReadS, readP_to_S, readS_to_P)
+import Text.ParserCombinators.ReadPrec as Exports (ReadPrec, readPrec_to_P, readP_to_Prec, readPrec_to_S, readS_to_Prec)
+import Text.Printf as Exports (printf, hPrintf)
+import Text.Read as Exports (Read(..), readMaybe, readEither)
+import Unsafe.Coerce as Exports
+
+-- contravariant
+-------------------------
+import Data.Functor.Contravariant as Exports
+import Data.Functor.Contravariant.Divisible as Exports
+
+-- pointed
+-------------------------
+import Data.Pointed as Exports
+import Data.Copointed as Exports
+
+-- profunctors
+-------------------------
+import Data.Profunctor.Unsafe as Exports
+import Data.Profunctor.Choice as Exports
+import Data.Profunctor.Strong as Exports
+
+-- semigroupoids
+-------------------------
+import Data.Semigroupoid as Exports
+
+-- cereal
+-------------------------
+import Data.Serialize as Exports (Serialize)
+
+-- deferred-folds
+-------------------------
+import DeferredFolds.Unfold as Exports (Unfold(..))
+import DeferredFolds.UnfoldM as Exports (UnfoldM(..))
+
+-- foldl
+-------------------------
+import Control.Foldl as Exports (Fold(..), FoldM(..))
+
+-- primitive
+-------------------------
+import Data.Primitive as Exports
+import Control.Monad.Primitive as Exports
+
+-- vector
+-------------------------
+import Data.Vector as Exports (Vector)
+
+-- primitive-extras
+-------------------------
+import PrimitiveExtras.Data as Exports
+
+-- text
+-------------------------
+import Data.Text as Exports (Text)
+
+import qualified Data.Vector.Unboxed as UnboxedVector
+
+type UnboxedVector = UnboxedVector.Vector
+
+{-# INLINE modifyTVar' #-}
+modifyTVar' :: TVar a -> (a -> a) -> STM ()
+modifyTVar' var f = do
+    x <- readTVar var
+    writeTVar var $! f x
+
+{-# INLINE forMToZero_ #-}
+forMToZero_ :: Applicative m => Int -> (Int -> m a) -> m ()
+forMToZero_ !startN f =
+  ($ pred startN) $ fix $ \loop !n -> if n >= 0 then f n *> loop (pred n) else pure ()
+
+{-# INLINE forMFromZero_ #-}
+forMFromZero_ :: Applicative m => Int -> (Int -> m a) -> m ()
+forMFromZero_ !endN f =
+  ($ 0) $ fix $ \loop !n -> if n < endN then f n *> loop (succ n) else pure ()
+
+(.) :: Semigroupoid s => s b c -> s a b -> s a c
+(.) = o
diff --git a/library/Edges/Types.hs b/library/Edges/Types.hs
new file mode 100644
--- /dev/null
+++ b/library/Edges/Types.hs
@@ -0,0 +1,14 @@
+module Edges.Types
+where
+
+import Edges.Prelude
+
+
+data Edges source target =
+  Edges
+    {-# UNPACK #-} !Int {-^ Target array size -}
+    {-# UNPACK #-} !(PrimMultiArray Word32)
+
+newtype Node entity = Node Int
+
+newtype NodeCounts entity = NodeCounts (PrimArray Word32)
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,79 @@
+module Main where
+
+import Prelude
+import Test.QuickCheck
+import Test.QuickCheck.Instances
+import Test.Tasty
+import Test.Tasty.Runners
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+import qualified Edges.Edges as A
+import qualified Edges.NodeCounts as B
+import qualified Edges.Node as C
+import qualified Data.Serialize as D
+
+
+main =
+  defaultMain $
+  testGroup "All tests" $
+  [
+    testGroup "Predefined bipartite" $ let
+      edgeList :: [(C.Node (Proxy 1), C.Node (Proxy 2))]
+      edgeList =
+        fmap (bimap C.Node C.Node) $
+        [
+          (0, 0),
+          (0, 1),
+          (0, 2),
+          (1, 0),
+          (1, 1),
+          (2, 0)
+        ]
+      (edges1, edges2) = A.listBipartite edgeList
+      in
+        [
+          testCase "Constructs the forward edges correctly" $ let
+            reconstructedEdgeList = A.toAssocList edges1
+            in assertEqual (show reconstructedEdgeList) edgeList reconstructedEdgeList
+          ,
+          testCase "Constructs the backward edges correctly" $ let
+            reconstructedEdgeList = sort $ A.toAssocList edges2
+            expectedEdgeList = sort $ fmap swap edgeList
+            in assertEqual (show reconstructedEdgeList) expectedEdgeList reconstructedEdgeList
+          ,
+          testGroup "Counting at depth" $ let
+            node = C.Node 1 :: C.Node (Proxy 1)
+            in
+              [
+                testCase "0" $ let
+                  nodeCountsList = B.node edges1 node & B.toList
+                  in assertEqual (show nodeCountsList) [0, 1, 0] nodeCountsList
+                ,
+                testCase "1, unoptimized" $ let
+                  nodeCountsList = B.node edges1 node & B.targets edges1 & B.toList
+                  in assertEqual (show nodeCountsList) [1, 1, 0] nodeCountsList
+                ,
+                testCase "1" $ let
+                  nodeCountsList = B.nodeTargets edges1 node & B.toList
+                  in assertEqual (show nodeCountsList) [1, 1, 0] nodeCountsList
+                ,
+                testCase "2" $ let
+                  nodeCountsList = B.nodeTargets edges1 node & B.targets edges2 & B.toList
+                  in assertEqual (show nodeCountsList) [2, 2, 1] nodeCountsList
+                ,
+                testCase "3" $ let
+                  nodeCountsList = B.nodeTargets edges1 node & B.targets edges2 & B.targets edges1 & B.toList
+                  in
+                    {-
+                    [0, 1]
+                    [[0, 1, 2], [0, 1]]
+                    [[[0, 1, 2], [0, 1], [0]], [[0, 1, 2], [0, 1]]]
+                    [5, 4, 2]
+                    -}
+                    assertEqual (show nodeCountsList) [5, 4, 2] nodeCountsList
+              ]
+        ]
+    ,
+    testProperty "Encoding/decoding with Cereal" $ forAll (A.genBipartiteWithLimits 10 20) $ \ (edges1, edges2) ->
+    D.decode (D.encode edges1) === Right edges1
+  ]
