kleisli: Kleisli-like newtypes with different type parameter orderings

[ bsd3, data, library ] [ Propose Tags ] [ Report a vulnerability ]

Three newtype wrappers around p a (f b) with different type parameter orderings, enabling different type class instances depending on which parameter is last:

  • Kleisli p a f b — functor in b (Functor, Applicative, Monad, etc.)

  • ProKleisli p f a b — profunctor in (a, b) (Profunctor, Category, Arrow, etc.)

  • ContraKleisli p b f a — contravariant in a (Contravariant, Divisible, Decidable)

All three are representationally identical and connected by isomorphisms. When p is specialised to (->), extensive instances are derived via Star, ReaderT, Arrow.Kleisli, and Op.


[Skip to Readme]

Modules

[Index] [Quick Jump]

Flags

Manual Flags

NameDescriptionDefault
dev

Enable development warnings (-Werror, -O2 for benchmarks)

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.0.1, 0.0.2, 0.0.3, 0.0.4, 0.0.5
Change log changelog.md
Dependencies adjunctions (>=4.3 && <5), base (>=4.8 && <6), comonad (>=5 && <6), contravariant (>=1 && <2), deepseq (>=1.4 && <2), distributive (>=0.5 && <1), lens (>=4 && <6), mtl (>=2.2 && <3), profunctors (>=5 && <6), selective (>=0.5 && <1), semigroupoids (>=5.2 && <7), transformers (>=0.5 && <1) [details]
Tested with ghc ==9.6.7
License BSD-3-Clause
Author Tony Morris <tmorris@tmorris.net>
Maintainer Tony Morris <tmorris@tmorris.net>
Uploaded by TonyMorris at 2026-05-27T06:20:37Z
Category Data
Home page https://gitlab.com/tonymorris/kleisli
Bug tracker https://gitlab.com/tonymorris/kleisli/-/issues
Source repo head: git clone https://gitlab.com/tonymorris/kleisli.git
Distributions
Reverse Dependencies 1 direct, 2 indirect [details]
Downloads 6 total (6 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2026-05-27 [all 1 reports]

Readme for kleisli-0.0.5

[back to package description]

kleisli

Three newtype wrappers around p a (f b) with different type parameter orderings, enabling different type class instances depending on which parameter is last.

Type Parameter order Primary instances
Kleisli p a f b functor in b Functor, Applicative, Monad, MonadTrans, Distributive, Representable
ProKleisli p f a b profunctor in (a, b) Profunctor, Strong, Choice, Closed, Category, Arrow, Sieve, Representable
ContraKleisli p b f a contravariant in a Contravariant, Divisible, Decidable

All three are representationally identical (p a (f b)) and connected by isomorphisms.

Usage

When p is specialised to (->), extensive instances are derived via Star, ReaderT, Arrow.Kleisli, and Op:

import Data.Kleisli

-- Kleisli: use as a functor/monad in the result
k :: Kleisli (->) Int Maybe Int
k = fmap (+1) (Kleisli Just)

-- ProKleisli: use as a profunctor/arrow
p :: ProKleisli (->) Maybe Int Int
p = arr (+1) >>> arr (*2)

-- ContraKleisli: use as a contravariant functor
c :: ContraKleisli (->) Int Maybe Int
c = contramap (+1) (ContraKleisli Just)

Type aliases

Convenient aliases eliminate common type parameters:

type Kleisli'  p a b = Kleisli p a Identity b       -- no functor layer
type KleisliA  a f b = Kleisli (->) a f b           -- specialise profunctor to (->)
type KleisliA' a b   = KleisliA a Identity b        -- both specialised

Analogous aliases exist for ProKleisli and ContraKleisli.

Isomorphisms

Lens Isos convert between the three orderings:

_Kleisli_ProKleisli    :: Iso (Kleisli p a f b) ... (ProKleisli p f a b) ...
_Kleisli_ContraKleisli :: Iso (Kleisli p a f b) ... (ContraKleisli p b f a) ...

Identity-eliminating isos map through the Identity wrapper:

kleisli' :: Iso (Kleisli' p a b) ... (p a b) ...

Design: types as interfaces

This library follows an approach where the data type is the API and type-class instances are the interface. There are almost no top-level functions — users interact with these newtypes entirely through standard algebraic vocabulary (fmap, >>=, lmap, arr, contramap, divide, etc.) that they already know.

How it works

A single representation (p a (f b)) is wrapped in three newtypes that differ only in which type parameter is "outermost" — i.e. which parameter appears last and therefore determines which type classes the newtype can inhabit. The newtypes carry no runtime payload beyond what p a (f b) already is; they exist solely to place the type in the right position for GHC's instance resolution to fire.

The few top-level values that do exist cover the gaps that type classes cannot express:

  • Isomorphisms — there is no standard IsomorphicTo class that yields both directions, so _Kleisli_ProKleisli and friends are explicit Iso values.
  • Natural-transformation maps (hoistKleisli, pureKleisli, etc.) — mapping over an inner functor layer is not captured by Functor (which operates on the outermost parameter), so a small family of combinators is provided.

Everything else is an instance.

Benefits

  • Vocabulary reuse. Users do not learn a new API. If you know Profunctor you already know how to use ProKleisli; if you know Divisible you already know how to use ContraKleisli. Every generic combinator, tutorial, and intuition that applies to those classes applies here without modification.

  • Composability for free. Because the interface is expressed through standard classes, values of these types compose with any other code written against those same abstractions — lens combinators, servant routing, contravariant logging, arrow notation, selective functors, and so on — with no adapter or conversion layer.

  • Minimal surface area. A small export list means fewer names to remember, fewer possible breakages across versions, and fewer degrees of freedom for API design mistakes. The type and its instances are the documentation.

  • Derivability. DerivingVia lets GHC generate correct, law-abiding instances mechanically from an existing carrier (Star, ReaderT, Arrow.Kleisli, Op). The module author writes each instance at most once; the compiler writes the rest.

Trade-offs

  • Discoverability. The real functionality lives in instance heads, not in a list of named functions. Users must know (or look up) which class provides the operation they want. Haddock instance lists help, but they are less immediately scannable than a flat function list.

  • Type errors. When an instance is missing or a constraint doesn't match, GHC's error messages refer to the class and the full newtype-wrapped type, which can be verbose. Type aliases help at use sites but cannot simplify error output.

  • Orphan-instance risk. If a user needs an instance that this library did not provide, they cannot add it without creating an orphan (since both the class and the type are defined elsewhere relative to the user). In practice this is mitigated by providing instances densely — if a lawful instance exists, this module should already have it.

Performance implications

All three newtypes are strict newtypes — they vanish at runtime under GHC's optimiser. The key mechanisms:

  • Newtype coercion. GHC represents Kleisli (->) a f b identically to a -> f b in memory. Wrapping, unwrapping, and converting between the three newtypes via the provided isos are zero-cost (they compile to id/coerce after optimisation).

  • INLINE pragmas. Every top-level combinator and every hand-written instance method carries an INLINE pragma, ensuring that after inlining the only code that remains is the user's function and the functor operations — no dictionary passing, no wrapper allocation.

  • DerivingVia and specialisation. Instances derived via Star, ReaderT, or Arrow.Kleisli are thin wrappers around those types' methods. With optimisation enabled (-O1 or above), GHC specialises and inlines through the via-type, eliminating it entirely. The generated Core is the same as if the instance had been written by hand.

  • No runtime indirection. Because there is no existential, no GADT, and no dictionary field stored in the type, using these newtypes has exactly the same runtime cost as using the underlying p a (f b) directly. The abstraction is purely a compile-time mechanism for selecting instances.

In summary: you pay nothing at runtime for the newtype layer. The only cost is at compile time — more instances means more constraint solving — but in practice this is negligible.

Building

cabal build
cabal test doctest
cabal bench