known-lists: Easy type-level lists with term-level membership proofs.

[ bsd3, data-structures, dependent-types, library, type-system ] [ Propose Tags ] [ Report a vulnerability ]

A type-level lists library for easy proof-of-membership programming. Provides data structures for talking about members of a list, as well as subsets, maps, and traversals.


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0
Dependencies base (>=4.20.0.0 && <4.21), bifunctors (>=5.0 && <6) [details]
License BSD-3-Clause
Author
Maintainer shapeofmatter@pm.me
Uploaded by ShapeOfMatter at 2026-01-30T14:46:01Z
Category Data Structures , Dependent Types , Type System
Source repo head: git clone https://github.com/ShapeOfMatter/known-lists
Distributions
Downloads 1 total (1 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 known-lists-0.1.0.0

[back to package description]

Known Lists

This library is intended to make basic operations with type-level lists easy. In particular, it's for situations in which membership of a type-level symbol (often, but not necessarily, a Symbol) in a list is a critical property to enforce.

Data.Known.Knowable generalises the idea of a "known" type-level symbol; a Known constraint can be used on Symbols, Nats, etc, and on lists of such Known types.
Data.Known.Membership contains the key structures and logic for enforcing membership constraints; the naive class-based solution (which is also included) fails in polymorphic code; this system allows more robust and explicit handling.
Data.Known.TypeIndexed introduces TIndexed, heterogeneous lists indexed by type-level symbols.

This library is an adaptation for general use of an ad hoc system originally used in MultiChor. Very likely everything in this library can, should, and eventually will be replaced by some correct combination of singletons, spo, and related libraries, but until there's good documentation showing new users how to do that, this library will be useful.

Examples

The entire MultiChor library can be viewed as an example use-case. Here we show a less involved example which relies on the (morally incidental) fact that Member objects are effectively finite Nats.

To begin with, consider the traditional Haskell fixed-length (length-indexed) vector:

data VecN (n :: GHC.TypeLits.Nat) a where
  NNil  :: VecN 0 a
  NCons :: a -> VecN n a -> VecN (n + 1) a

indexN :: VecN n a -> Int -> a  -- UNSAFE: Partial!
indexN (NCons x _) 0          = x
indexN (NCons _ xs) i | 0 < i = indexN xs (i - 1)
indexN _ _                    = error "indexN: Out of bounds."

Using a partial function for indexing isn't satisfying. We can improve on this situation by using lists of Unit and corresponding Member values as Nats and Finite Nats:

type Nat = [()]

data VecU (n :: Nat) a where
  UNil  :: VecU '[] a
  UCons :: a -> VecU n a -> VecU ('() ': n) a

indexU :: VecU n a -> Member '() n -> a
indexU (UCons x _) First = x
indexU (UCons _ xs) (Later i) = indexU xs i
indexU UNil i = case i of {}  -- This index function is safe; GHC knows there is no such `i`.

But there's no need to actually write out this simple example like that; known-lists gives such a type out-of-the-box:

type Vec (n :: Nat) a = TVec n a

We can extend this example to ragged matrices or arrays. Note that RaggedMatrix can't be expressed using just TVec; the rows have different lengths and the length is encoded in the type, so each row actually has a distinct type.

newtype RaggedMatrix (rows :: [Nat]) a = Ragged {
    raggedIndexes :: TIndexed rows (Flip TVec a)
  }

(!!!) :: (Known Nat row)
      => RaggedMatrix rows a
      -> (Member row rows, Member '() row)
      -> a
(!!!) (Ragged TIndexed{tindex}) (row, i) = runFlip (tindex row) ! i

Flattening a RaggedMatrix into a list is short and sweet. Populating a RaggedMatrix from a flat list demonstrates the use of tySpine.

raggedToList :: forall rows a. (Known [Nat] rows)
             => RaggedMatrix rows a -> [a]
raggedToList (Ragged (TIndexed f)) = concat lists
  where lists :: TVec rows [a]
        lists = pack (toList . runFlip . f)

raggedFromList :: forall rows a. (Known [Nat] rows)
               => [a] -> RaggedMatrix rows a
raggedFromList xs = case tySpine @Nat @rows of
  TyNil -> Ragged $ TIndexed \case{}
  TyCons (_ :: Proxy r1) (_ :: Proxy rows') ->
                let (xs1, xsTail) = splitAt (knownLength $ allOf @r1) xs
                    -- This is where it will crash if the list isn't big enough:
                    r1 :: TVec r1 a
                    r1 = EXTS.fromList $ zip (repeat ()) xs1
                    f :: TIndex rows' (Flip TVec a)
                    Ragged TIndexed{tindex=f} = raggedFromList xsTail
                in Ragged $ TIndexed \case
                    First -> Flip r1
                    Later i -> f i

sequenceT allows us to sequence uniform monadic effects over heterogeneous TIndexed structures. In this example, instead of building the RaggedMatrix from a list, we read the elements from stdIn one at a time. Since the exact type of the IO action needed for each row is different, we use sequenceT.

raggedFromStdIn :: forall rows a. (Read a, Known [Nat] rows)
                => IO (RaggedMatrix rows a)
raggedFromStdIn = Ragged <$> rfsi
  where rfsi :: IO (TIndexed rows (Flip TVec a))
        rfsi = sequenceT $ TIndexed $ Compose <$> rows
        rows :: forall row. (Known Nat row)
             => Member row rows -> IO (Flip TVec a row)
        rows _ = Flip <$> row
        row :: forall row. (Known Nat row) => IO (TVec row a)
        row = fmap TVec $ sequenceT $ TIndexed $
                  Compose . fmap Const <$> item
        item :: forall i row. Member i row -> IO a
        item = const readLn