packages feed

distributors-0.4.0.0: src/Control/Lens/PartialIso.hs

{- |
Module      : Control.Lens.PartialIso
Description : partial isomorphisms
Copyright   : (C) 2026 - Eitan Chatav
License     : BSD-style (see the file LICENSE)
Maintainer  : Eitan Chatav <eitan.chatav@gmail.com>
Stability   : provisional
Portability : non-portable

See Rendel & Ostermann,
[Invertible syntax descriptions](https://www.informatik.uni-marburg.de/~rendel/unparse/)
-}

{-# OPTIONS_GHC -Wno-orphans #-}

module Control.Lens.PartialIso
  ( -- * PartialIso
    dimapMaybe
  , PartialIso
  , PartialIso'
  , APartialIso
  , PartialExchange (PartialExchange)
    -- * Combinators
  , partialIso
  , partialInvoluted
  , withPartialIso
  , clonePartialIso
  , coPartialIso
  , crossPartialIso
  , altPartialIso
    -- * Applicators
  , (>?)
  , (?<)
  , (>?<)
  , (>~)
  , (~<)
  , coPrism
    -- * Patterns
  , satisfied
  , nulled
  , notNulled
  , eotMaybe
  , eotList
    -- * Iterators
  , iterating
  , difoldl1
  , difoldr1
  , difoldl
  , difoldr
    -- * Template Haskell
  , makeNestedPrisms
    -- * Re-exports
  , module Control.Lens.Iso
  , module Control.Lens.Prism
  ) where

import Control.Lens
import Control.Lens.Internal.NestedPrismTH
import Control.Lens.Internal.Profunctor
import Control.Lens.Iso
import Control.Lens.Prism
import Control.Monad
import Data.Functor.Compose
import Data.Profunctor
import Data.Profunctor.Monad
import Data.Profunctor.Yoneda
import Witherable

{- | The `dimapMaybe` function endows
`Choice` & `Cochoice` "partial profunctors"
with an action `>?<` of `PartialIso`s.
-}
dimapMaybe
  :: (Choice p, Cochoice p)
  => (s -> Maybe a) -> (b -> Maybe t)
  -> p a b -> p s t
dimapMaybe f g =
  let
    m2e h = maybe (Left ()) Right . h
    fg = dimap (>>= m2e f) (>>= m2e g)
  in
    unright . fg . right'

{- | `PartialIso` is a first class inexhaustive pattern,
similar to how `Control.Lens.Prism.Prism` is a first class exhaustive pattern,
by combining `Control.Lens.Prism.Prism`s and `coPrism`s.

Every `Control.Lens.Iso.Iso` & `Control.Lens.Prism.Prism` is `APartialIso`.

`PartialIso`s are isomorphic to `PartialExchange`s.
-}
type PartialIso s t a b = forall p f.
  (Choice p, Cochoice p, Applicative f, Filterable f)
    => p a (f b) -> p s (f t)

{- |
A simple `PartialIso'` @s a@ is an identification of
a subset of @s@ with a subset of @a@.

Given a simple `PartialIso'`, @partialIso f g@, has properties:

prop> Just = f <=< g
prop> Just = g <=< f

These are left and right inverse laws for proper `PartialIso'`s.
However, sometimes an improper `PartialIso'` will be useful.
For an improper `PartialIso'`, only the left inverse law holds.

prop> Just = f <=< g

For an improper `PartialIso'`, @norm = g <=< f@ is an idempotent

prop> norm = norm <=< norm

and can be regarded as a normalization within
some equivalence class of terms.
-}
type PartialIso' s a = PartialIso s s a a

{- | `APartialIso` is monomorphically a `PartialIso`. -}
type APartialIso s t a b =
  PartialExchange a b a (Maybe b) -> PartialExchange a b s (Maybe t)

{- | A `PartialExchange` provides efficient access
to the two functions that make up a `PartialIso`.
-}
data PartialExchange a b s t =
  PartialExchange (s -> Maybe a) (b -> Maybe t)
instance Functor (PartialExchange a b s) where fmap = rmap
instance Filterable (PartialExchange a b s) where
  mapMaybe = dimapMaybe Just
instance Profunctor (PartialExchange a b) where
  dimap f' g' (PartialExchange f g) =
    PartialExchange (f . f') (fmap g' . g)
instance Choice (PartialExchange a b) where
  left' (PartialExchange f g) =
    PartialExchange (either f (pure Nothing)) ((Left <$>) . g)
  right' (PartialExchange f g) =
    PartialExchange (either (pure Nothing) f) ((Right <$>) . g)
instance Cochoice (PartialExchange a b) where
  unleft (PartialExchange f g) =
    PartialExchange (f . Left) (either Just (pure Nothing) <=< g)
  unright (PartialExchange f g) =
    PartialExchange (f . Right) (either (pure Nothing) Just <=< g)

{- | Build a `PartialIso`. -}
partialIso :: (s -> Maybe a) -> (b -> Maybe t) -> PartialIso s t a b
partialIso f g =
  unright . iso (maybe (Left ()) Right . f =<<) (mapMaybe g) . right'

{- | Given a partial function that is its own inverse,
this gives you a `PartialIso'` using it in both directions. -}
partialInvoluted :: (a -> Maybe a) -> PartialIso' a a
partialInvoluted f = partialIso f f

{- | Convert `APartialIso` to the pair of functions that characterize it. -}
withPartialIso
  :: APartialIso s t a b
  -> ((s -> Maybe a) -> (b -> Maybe t) -> r)
  -> r
withPartialIso pattern k =
  case pattern (PartialExchange Just (Just . Just)) of
    PartialExchange f g -> k f (join . g)

{- | Clone `APartialIso` so that you can reuse the same
monomorphically typed partial isomorphism for different purposes.
-}
clonePartialIso
  :: APartialIso s t a b
  -> PartialIso s t a b
clonePartialIso pattern = withPartialIso pattern $ \f g -> partialIso f g

{- | Clone and invert `APartialIso`. -}
coPartialIso
  :: APartialIso b a t s
  -> PartialIso s t a b
coPartialIso pattern =
  withPartialIso pattern $ \f g -> partialIso g f

{- | Construct a `PartialIso` on pairs from components. -}
crossPartialIso
  :: APartialIso s t a b
  -> APartialIso u v c d
  -> PartialIso (s,u) (t,v) (a,c) (b,d)
crossPartialIso x y =
  withPartialIso x $ \e f ->
  withPartialIso y $ \g h ->
    partialIso
      (\(s,u) -> (,) <$> e s <*> g u)
      (\(t,v) -> (,) <$> f t <*> h v)

{- | Construct a `PartialIso` on `Either`s from components. -}
altPartialIso
  :: APartialIso s t a b
  -> APartialIso u v c d
  -> PartialIso
      (Either s u) (Either t v)
      (Either a c) (Either b d)
altPartialIso x y =
  withPartialIso x $ \e f ->
  withPartialIso y $ \g h ->
    partialIso
      (either ((Left <$>) . e) ((Right <$>) . g))
      (either ((Left <$>) . f) ((Right <$>) . h))

{- | Action of `APrism` on `Choice` `Profunctor`s. -}
(>?)
  :: Choice p
  => APrism s t a b
  -> p a b
  -> p s t
(>?) pat = withPrism pat $ \f g -> dimap g (either id f) . right'
infixl 4 >?

{- | Action of a coPrism on `Cochoice` `Profunctor`s. -}
(?<)
  :: Cochoice p
  => APrism b a t s
  -> p a b
  -> p s t
(?<) pat = withPrism pat $ \f g -> unright . dimap (either id f) g
infixl 4 ?<

{- | Action of `APartialIso` on `Choice` & `Cochoice` partial profunctors. -}
(>?<)
  :: (Choice p, Cochoice p)
  => APartialIso s t a b
  -> p a b
  -> p s t
(>?<) pat = withPartialIso pat dimapMaybe
infixl 4 >?<

{- | Action of `AnIso` on `Profunctor`s. -}
(>~) :: Profunctor p => AnIso s t a b -> p a b -> p s t
(>~) pattern = withIso pattern dimap
infixl 2 >~

{- | Inverse action of `AnIso` on `Profunctor`s. -}
(~<) :: Profunctor p => AnIso b a t s -> p a b -> p s t
(~<) pattern = withIso pattern (flip dimap)
infixl 2 ~<

{- | Action of a `coPrism`
on the composition of a `Profunctor` and `Filterable`.
-}
coPrism :: (Profunctor p, Filterable f) => APrism b a t s -> p a (f b) -> p s (f t)
coPrism p = unwrapPafb . (?<) p . WrapPafb

{- | `satisfied` is the prototypical proper partial isomorphism,
identifying a subset which satisfies a predicate. -}
satisfied :: (a -> Bool) -> PartialIso' a a
satisfied f = partialInvoluted satiate where
  satiate a = if f a then Just a else Nothing

{- | `nulled` matches an `Empty` pattern, like `_Empty`. -}
nulled :: (AsEmpty s, AsEmpty t) => PartialIso s t () ()
nulled = partialIso empA empB where
  empA s = if isn't _Empty s then Nothing else Just ()
  empB _ = Just Empty

{- | `notNulled` matches a non-`Empty` pattern. -}
notNulled :: (AsEmpty s, AsEmpty t) => PartialIso s t s t
notNulled = partialIso nonEmp nonEmp where
  nonEmp s = if isn't _Empty s then Just s else Nothing

{- | The either-of-tuples representation of `Maybe`. -}
eotMaybe :: Iso (Maybe a) (Maybe b) (Either a ()) (Either b ())
eotMaybe = iso
  (maybe (Right ()) Left)
  (either Just (const Nothing))

{- | The either-of-tuples representation of list-like streams. -}
eotList
  :: (Cons s s a a, AsEmpty t, Cons t t b b)
  => Iso s t (Either (a,s) ()) (Either (b,t) ())
eotList = iso
  (maybe (Right ()) Left . uncons)
  (either (review _Cons) (const Empty))

{- | Iterate the application of a partial isomorphism,
useful for constructing fold/unfold isomorphisms. -}
iterating :: APartialIso a b a b -> Iso a b a b
iterating pattern = withPartialIso pattern $ \f g ->
  iso (iter f) (iter g) where
    iter h state = maybe state (iter h) (h state)

{- | Left fold & unfold `APartialIso` to an `Control.Lens.Iso.Iso`. -}
difoldl1
  :: Cons s t a b
  => APartialIso d c (d,b) (c,a)
  -> Iso (d,t) (c,s) (d,t) (c,s)
difoldl1 pattern =
  let
    associate = iso
      (\(c,(a,s)) -> ((c,a),s))
      (\((d,b),t) -> (d,(b,t)))
    step
      = crossPartialIso id _Cons
      . associate
      . crossPartialIso (coPartialIso pattern) id
  in from (iterating step)

{- | Right fold & unfold `APartialIso` to an `Control.Lens.Iso.Iso`. -}
difoldr1
  :: Cons s t a b
  => APartialIso d c (b,d) (a,c)
  -> Iso (t,d) (s,c) (t,d) (s,c)
difoldr1 pattern =
  let
    reorder = iso
      (\((a,s),c) -> (s,(a,c)))
      (\(t,(b,d)) -> ((b,t),d))
    step
      = crossPartialIso _Cons id
      . reorder
      . crossPartialIso id (coPartialIso pattern)
  in from (iterating step)

{- | Left fold & unfold `APartialIso` to a `Control.Lens.Prism.Prism`. -}
difoldl
  :: (AsEmpty t, Cons s t a b)
  => APartialIso d c (d,b) (c,a)
  -> Prism d c (d,t) (c,s)
difoldl pattern
  = dimap (,Empty) (fmap fst)
  . difoldl1 pattern

{- | Right fold & unfold `APartialIso` to a `Control.Lens.Prism.Prism`. -}
difoldr
  :: (AsEmpty t, Cons s t a b)
  => APartialIso d c (b,d) (a,c)
  -> Prism d c (t,d) (s,c)
difoldr pattern
  = dimap (Empty,) (fmap snd)
  . difoldr1 pattern

-- Orphanage --

instance (Profunctor p, Functor f)
  => Functor (WrappedPafb f p a) where fmap = rmap
deriving via Compose (p a) f instance
  (Profunctor p, Functor (p a), Filterable f)
    => Filterable (WrappedPafb f p a)
instance (Profunctor p, Filterable f)
  => Cochoice (WrappedPafb f p) where
    unleft (WrapPafb p) = WrapPafb $
      dimap Left (mapMaybe (either Just (const Nothing))) p
    unright (WrapPafb p) = WrapPafb $
      dimap Right (mapMaybe (either (const Nothing) Just)) p
instance (Profunctor p, Filterable (p a))
  => Filterable (Yoneda p a) where
    catMaybes = proreturn . catMaybes . proextract
instance (Profunctor p, Filterable (p a))
  => Filterable (Coyoneda p a) where
    catMaybes = proreturn . catMaybes . proextract
instance Filterable (Forget r a) where
  catMaybes (Forget f) = Forget f
instance Filterable f => Filterable (Star f a) where
  catMaybes (Star f) = Star (catMaybes . f)