proarrow: Category theory with a central role for profunctors

[ bsd3, categories, library, math ] [ Propose Tags ] [ Report a vulnerability ]

A library for doing category theory in Haskell with profunctors, rather than functors, as the central abstraction. Every Haskell kind carries at most one category structure (chosen via CategoryOf), newtype wrappers on kinds give variant categories, and functors are encoded as representable profunctors. On top of this the library provides monoidal structure, (co)limits, adjunctions, Kan extensions, promonads, and a full profunctor-optics hierarchy. . Import Proarrow to get started; Proarrow.Core explains the design of the core abstractions in depth. The public sublibrary proarrow:testing provides generic law-checking properties for testing your own categories.


[Skip to Readme]

library proarrow

Modules

[Index]

library proarrow:testing

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

Versions [RSS] 0.1.0.0
Change log CHANGELOG.md
Dependencies base (>=4.20 && <5), containers (>=0.6 && <0.9), data-default (>=0.7 && <0.9), falsify (>=0.4 && <0.5), fin (>=0.3.2 && <1), proarrow, tasty (>=1.4 && <1.6), tasty-falsify (>=0.1 && <0.2), universe-base (>=1.1.4 && <1.2), vec (>=0.5.1 && <1) [details]
Tested with ghc ==9.10.3 || ==9.12.2
License BSD-3-Clause
Author Sjoerd Visscher
Maintainer sjoerd@w3future.com
Uploaded by SjoerdVisscher at 2026-09-28T14:13:18Z
Category Math, Categories
Home page https://github.com/sjoerdvisscher/proarrow
Bug tracker https://github.com/sjoerdvisscher/proarrow/issues
Distributions
Downloads 5 total (5 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for proarrow-0.1.0.0

[back to package description]

Haskell-CI

proarrow

A Haskell library for doing category theory with a central role for profunctors.

Core ideas

One category per kind

Kind-indexed categories makes life a lot easier, once you know what the kind is of a type, you know which category it belongs to.

Use newtype wrappers on kinds

Using kind-indexed categories means you cannot share objects between categories. Newtype wrappers fix this. For example, if you have a category for kind k, it's opposite category has kind OP k.

Kind j -> k -> Type is reserved for profunctors

If profunctors would have kind OP j -> k -> Type, then (->) wouldn't be a profunctor as is. This would require too many wrappers all over the place. So instead j -> k -> Type is reserved for profunctors. So for the category of bifunctors we do need a wrapper.

Use constraints to limit which objects are part of a category

You need this already when creating a category of functors, then each object needs a Functor constraint. It turns out this is powerful enough to limit the objects of any type of category.

These constraints can be observed from arrows

If you're not careful these objects constraints can become unweildy, requiring a long list of object constraints for each function. But if you have an arrow from a to b, that's proof enough that a and b are objects. So there are functions (//) and (\\) to observe the constraints.

Functors that don't land in Type are written as representable profunctors

Functors have kind j -> k, but you can't just make a datatype of any kind, it must always be of the shape j -> k -> ... -> Type. So for example you can't make an identity functor that works for any k. But functors are isomorphic to representable profunctors, with kind k -> j -> Type. (Note that the kinds swap!) So you can write an identity representable profunctor!

Generalize the category theory to work with profunctors

To make working with representable profunctors instead of functors easier, the category theory should work with profunctors where possible.

Example: defining your own category

A category is picked out by its kind, so a new category starts with a fresh kind -- here one with two objects, a Draft and a Live state, and a single non-identity arrow publishing the one as the other.

{-# LANGUAGE TypeData, TypeFamilies #-}
import Prelude hiding (id, (.))

import Proarrow.Core (CAT, CategoryOf (..), ObId (..), Profunctor (..), Promonad (..), dimapDefault)

type data STATE = Draft | Live

type Move :: CAT STATE
data Move a b where
  KeepDraft :: Move Draft Draft
  Publish :: Move Draft Live
  KeepLive :: Move Live Live

deriving instance Show (Move a b)

-- 'id' has to produce the identity *at whichever object it is asked for*, so being an
-- object is exactly the ability to supply that identity:
instance ObId Draft where objId = KeepDraft
instance ObId Live where objId = KeepLive

instance CategoryOf STATE where
  type (~>) = Move

instance Promonad Move where
  KeepDraft . KeepDraft = KeepDraft
  Publish . KeepDraft = Publish
  KeepLive . Publish = Publish
  KeepLive . KeepLive = KeepLive

instance Profunctor Move where
  dimap = dimapDefault
  r \\ KeepDraft = r
  r \\ Publish = r
  r \\ KeepLive = r

Beyond TypeData and TypeFamilies above, going further needs more extensions — a Proarrow.Testing.TestableType instance for Move a b, for instance, also needs UndecidableInstances. Rather than discovering them one failed build at a time, enable the set the library itself is built with: GHC2024 plus the default-extensions block in proarrow.cabal.

>>> KeepLive . Publish . id
Publish

The Ob family is where the object constraints from above come in, and the \\ method is how those constraints are observed from an arrow — matching on a constructor reveals which objects it runs between. Note what is absent: no type Ob, and no id. Ob defaults to ObId, and id defaults to objId, so the two instances above are the whole of the object structure. A category where every type of the kind is an object with no evidence needed says type Ob a = Any a instead — that is what Hask does. Where the objects carry no non-identity arrows at all, reach for Proarrow.Category.Instance.Discrete's DISCRETE (see test/Props/Paths.hs), and a one-object category needs no dispatch at all, being just a monoid — Proarrow.Category.Instance.Monoid.

And now the generic kind-machinery applies: OPPOSITE STATE is the opposite category, (STATE, STATE) the product category, STATE +-> STATE are profunctors on states, and so on. The Proarrow module exports the curated core vocabulary; Proarrow.Core explains the design in depth, and the Proarrow.Category.Instance.* modules contain many more worked examples of categories.

To property-test the laws of your own category, depend on the public sublibrary proarrow:testing: a Testable instance for your kind plus the law checks from Proarrow.Testing.Laws (testCategory, testMonoidal, ...) give it a test suite — proarrow's own tests are built from exactly these pieces.

Laws as code

A class's laws are written down next to the class, as ordinary proarrow code that works in any category with the structure: a Laws instance from Proarrow.Tools.Laws, keyed by the list of structures the laws mention. For example, from Proarrow.Category.Monoidal, where SymMonoidalStructures is '[Monoidal, SymMonoidal]:

instance Laws SymMonoidalStructures where
  laws =
    [ ...
    , Law "swap naturality" \ @a @b @c @d mor -> do
        f <- mor @a @c "f"
        g <- mor @b @d "g"
        swap @_ @c @d . (f ** g) === (g ** f) . swap @_ @a @b
    , ...
    ]

A law binds the object variables it uses and asks the supply mor for named arbitrary arrows between them, then states its equation with ===. testLaws from Proarrow.Testing.Laws.Run checks each law as its own property. It draws random objects and arrows, runs the law in a category whose arrows also describe themselves, and on failure prints both sides as the code they were built from:

Failed swap naturality:
swap . (f ** g) = ...
(g ** f) . swap = ...

testMonoidal, testClosed and the other checks for proarrow's own classes are built this way, and a class of your own can be checked the same way: test/Examples/CustomLaws.hs walks through a complete one.

Laws as code × code as diagrams = laws as diagrams

Proarrow.Tools.Diagrams.Svg defines a category SVG whose objects are lists of wires and whose arrows are string diagrams. It is symmetric monoidal, closed, *-autonomous and compact closed, has traces, and can copy and merge wires, so code written for any category with that structure also runs in SVG, where it builds a picture of itself. node "f" is a box named f, and render turns a diagram into an SVG image. The layout follows how the diagram was built: a tensor puts its two sides next to each other, a composite stacks them with a band of curved wires in between, and a trace draws its loops round the side. Options can draw more of the structure, such as the identities, the unitors and associators, or the swaps.

Laws are code too, so running one in SVG draws it: lawSvgs @'[Monoidal] gives every law of Monoidal as a named SVG, an equation between its two sides, each drawn the way the law builds it. This is swap naturality from above, drawn with explicit swaps:

swap naturality

proLawSvgs does the same for profunctor classes such as Strong. The Law diagrams wiki page shows the laws of proarrow's classes drawn this way.