From ced30ee6c27e6562be0a5f07922bf940a9aa4ed9 Mon Sep 17 00:00:00 2001 From: Jozef Koval Date: Tue, 9 Jun 2026 17:50:36 +0200 Subject: [PATCH 1/2] Document the laws of the -WithIndex classes Add the compatibility laws (fmap = imap . const, foldMap = ifoldMap . const, traverse = itraverse . const), the agreement between imap/ifoldMap and their itraverse-derived defaults (imapDefault/ifoldMapDefault), and an index uniqueness/stability note to the FunctorWithIndex, FoldableWithIndex, and TraversableWithIndex class docstrings. Resolves ekmett/lens#745. --- indexed-traversable/Changelog.md | 7 +++++ indexed-traversable/src/WithIndex.hs | 41 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/indexed-traversable/Changelog.md b/indexed-traversable/Changelog.md index 4b06c38..78c6304 100644 --- a/indexed-traversable/Changelog.md +++ b/indexed-traversable/Changelog.md @@ -1,3 +1,10 @@ +# 0.1.6 [unreleased] + +- Document the laws of `FunctorWithIndex`, `FoldableWithIndex`, and + `TraversableWithIndex`: the `fmap`/`foldMap`/`traverse` compatibility laws, + agreement with `imapDefault`/`ifoldMapDefault`, and index uniqueness/stability. + Resolves . + # 0.1.5 [2026-05-07] - Use automatic flags for compatibility conditionals diff --git a/indexed-traversable/src/WithIndex.hs b/indexed-traversable/src/WithIndex.hs index eae947a..a023c44 100644 --- a/indexed-traversable/src/WithIndex.hs +++ b/indexed-traversable/src/WithIndex.hs @@ -68,6 +68,18 @@ import CoerceCompat -- 'imap' f '.' 'imap' g ≡ 'imap' (\\i -> f i '.' g i) -- 'imap' (\\_ a -> a) ≡ 'id' -- @ +-- +-- The index must be compatible with the underlying 'Functor': forgetting the +-- index recovers 'fmap'. +-- +-- @ +-- 'fmap' f ≡ 'imap' ('const' f) +-- @ +-- +-- The index of an element is determined by its position in the structure, not +-- by the element's value, so 'imap' leaves the indices unchanged. The indices +-- visited within a single structure should be distinct, and are the same +-- indices, in the same order, used by 'ifoldMap' and 'itraverse'. class Functor f => FunctorWithIndex i f | f -> i where -- | Map with access to the index. imap :: (i -> a -> b) -> f a -> f b @@ -86,6 +98,17 @@ imapDefault f = runIdentity #. itraverse (Identity #.. f) ------------------------------------------------------------------------------- -- | A container that supports folding with an additional index. +-- +-- The index must be compatible with the underlying 'Foldable': forgetting the +-- index recovers 'foldMap'. +-- +-- @ +-- 'foldMap' f ≡ 'ifoldMap' ('const' f) +-- @ +-- +-- When the same type is also a 'FunctorWithIndex' or 'TraversableWithIndex', +-- all three must agree on the index assigned to each element (see +-- 'FunctorWithIndex'). class Foldable f => FoldableWithIndex i f | f -> i where -- -- | Fold a container by mapping value to an arbitrary 'Monoid' with access to the index @i@. @@ -230,6 +253,24 @@ class (Foldable1 f, FoldableWithIndex i f) => Foldable1WithIndex i f | f -> i wh -- 'itraverse' ('const' 'Identity') ≡ 'Identity' -- 'fmap' ('itraverse' f) '.' 'itraverse' g ≡ 'Data.Functor.Compose.getCompose' '.' 'itraverse' (\\i -> 'Data.Functor.Compose.Compose' '.' 'fmap' (f i) '.' g i) -- @ +-- +-- The index must be compatible with the underlying 'Traversable': forgetting +-- the index recovers 'traverse'. +-- +-- @ +-- 'traverse' f ≡ 'itraverse' ('const' f) +-- @ +-- +-- The 'FunctorWithIndex' and 'FoldableWithIndex' instances must agree with the +-- ones induced by 'itraverse': +-- +-- @ +-- 'imap' ≡ 'imapDefault' +-- 'ifoldMap' ≡ 'ifoldMapDefault' +-- @ +-- +-- so 'itraverse' visits every element exactly once, in order, pairing it with +-- the same index used by 'imap' and 'ifoldMap' (see 'FunctorWithIndex'). class (FunctorWithIndex i t, FoldableWithIndex i t, Traversable t) => TraversableWithIndex i t | t -> i where -- | Traverse an indexed container. -- From 447907be903034da668dd5fcc24ffe56bdb9a46f Mon Sep 17 00:00:00 2001 From: Jozef Koval Date: Fri, 19 Jun 2026 23:22:04 +0200 Subject: [PATCH 2/2] Drop the index-uniqueness claim and the lens issue reference per review --- indexed-traversable/Changelog.md | 5 ++--- indexed-traversable/src/WithIndex.hs | 10 ++-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/indexed-traversable/Changelog.md b/indexed-traversable/Changelog.md index 78c6304..e7901d5 100644 --- a/indexed-traversable/Changelog.md +++ b/indexed-traversable/Changelog.md @@ -1,9 +1,8 @@ # 0.1.6 [unreleased] - Document the laws of `FunctorWithIndex`, `FoldableWithIndex`, and - `TraversableWithIndex`: the `fmap`/`foldMap`/`traverse` compatibility laws, - agreement with `imapDefault`/`ifoldMapDefault`, and index uniqueness/stability. - Resolves . + `TraversableWithIndex`: the `fmap`/`foldMap`/`traverse` compatibility laws + and agreement with `imapDefault`/`ifoldMapDefault`. # 0.1.5 [2026-05-07] diff --git a/indexed-traversable/src/WithIndex.hs b/indexed-traversable/src/WithIndex.hs index a023c44..f2b5e3d 100644 --- a/indexed-traversable/src/WithIndex.hs +++ b/indexed-traversable/src/WithIndex.hs @@ -75,11 +75,6 @@ import CoerceCompat -- @ -- 'fmap' f ≡ 'imap' ('const' f) -- @ --- --- The index of an element is determined by its position in the structure, not --- by the element's value, so 'imap' leaves the indices unchanged. The indices --- visited within a single structure should be distinct, and are the same --- indices, in the same order, used by 'ifoldMap' and 'itraverse'. class Functor f => FunctorWithIndex i f | f -> i where -- | Map with access to the index. imap :: (i -> a -> b) -> f a -> f b @@ -107,8 +102,7 @@ imapDefault f = runIdentity #. itraverse (Identity #.. f) -- @ -- -- When the same type is also a 'FunctorWithIndex' or 'TraversableWithIndex', --- all three must agree on the index assigned to each element (see --- 'FunctorWithIndex'). +-- all three agree on the index assigned to each element. class Foldable f => FoldableWithIndex i f | f -> i where -- -- | Fold a container by mapping value to an arbitrary 'Monoid' with access to the index @i@. @@ -270,7 +264,7 @@ class (Foldable1 f, FoldableWithIndex i f) => Foldable1WithIndex i f | f -> i wh -- @ -- -- so 'itraverse' visits every element exactly once, in order, pairing it with --- the same index used by 'imap' and 'ifoldMap' (see 'FunctorWithIndex'). +-- the same index used by 'imap' and 'ifoldMap'. class (FunctorWithIndex i t, FoldableWithIndex i t, Traversable t) => TraversableWithIndex i t | t -> i where -- | Traverse an indexed container. --