packages feed

filepather-0.8.0.0: src/System/FilePather/FilePather.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
-- Required for the MonadState, MonadWriter and MonadError instances: the
-- functional dependency of each class (for example, m -> s) is not covered by
-- the instance head MonadState s (FilePather f), because s does not occur in
-- FilePather f.
-- Terminates: each instance context is the same class at f, which is a strict
-- subterm of the instance head FilePather f, so resolution moves to a smaller
-- type at each step.
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wall #-}

{- | A 'FilePather' is a computation that depends on the file path being
operated on, producing a value in a type constructor @f@.

@
newtype FilePather f a = FilePather (FilePath -> f a)
@

A 'FilePather' is a product with one field, named 'pathFunction'. It is
isomorphic to @ReaderT FilePath f a@ (see
'filePatherReaderT'), and has the same instances. In particular, when @f@ is a
'Monad', so is @FilePather f@, and 'Control.Monad.Reader.ask' returns the file path being operated
on.
-}
module System.FilePather.FilePather (
  -- * Types
  FilePather (..),

  -- * Type aliases
  FilePather',
  FilePatherIO,
  FilePatherMaybe,
  FilePatherList,
  FilePatherNonEmpty,
  FilePatherEither,
  FilePatherMaybeT,
  FilePatherExceptT,
  FilePatherExitcode,
  FilePatherExitcode',

  -- * Isomorphisms
  filePatherReaderT,

  -- * Optics
  FilePatherLike (..),
  lawNamesFilePather,
  InheritFilePather,
  inheritFilePather,

  -- * Fusion workers
  mapFilePather,
  mapMaybeFilePather,
) where

import Control.Applicative (Alternative)
import Control.DeepSeq (NFData)
import Control.Exitcode (Exitcode)
import Control.Lens (Iso, Optic, cloneLens, iso, view)
import Control.Monad (MonadPlus)
import Control.Monad.Cont.Class (MonadCont)
import Control.Monad.Error.Class (MonadError)
import Control.Monad.Fix (MonadFix)
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Reader (ReaderT (..))
import Control.Monad.Reader.Class (MonadReader)
import Control.Monad.State.Class (MonadState)
import Control.Monad.Trans.Class (MonadTrans)
import Control.Monad.Trans.Except (ExceptT)
import Control.Monad.Trans.Maybe (MaybeT)
import Control.Monad.Writer.Class (MonadWriter)
import Control.Monad.Zip (MonadZip)
import Control.Selective (Selective)
import Data.Distributive (Distributive (..))
import Data.Functor.Alt (Alt (..))
import Data.Functor.Apply (Apply)
import Data.Functor.Bind (Bind (..))
import Data.Functor.Const (Const)
import Data.Functor.Contravariant (Contravariant)
import Data.Functor.Contravariant.Divisible (Decidable, Divisible)
import Data.Functor.Identity (Identity)
import Data.Functor.Plus (Plus)
import Data.Functor.Rep (Representable (..))
import Data.Kind (Type)
import Data.List.NonEmpty (NonEmpty)
import GHC.Generics (Generic, Generic1)
import System.FilePather.Name.PathFunction (PathFunctionLike (..))
import Witherable (Filterable (..))

{- $setup
>>> import Control.Lens (cloneLens, view)
>>> import Control.Monad.Reader (ask, asks)
>>> import Data.Functor.Identity (Identity (..))
>>> import System.FilePath (takeExtension)
>>> import Witherable (mapMaybe)
-}

{- | A computation that depends on the file path being operated on.

The file path is typically an input file, and the result is typically one or
more output files, or some information about the input file. Use 'Control.Monad.Reader.ask' for the
file path, and 'Control.Monad.Reader.local' to operate on a different file path.

==== __Examples__

>>> runIdentity (view (cloneLens pathFunction) (asks takeExtension :: FilePather' String) "notes.md")
".md"

>>> view (cloneLens pathFunction) (FilePather (\p -> [p, p ++ ".bak"])) "notes.md"
["notes.md","notes.md.bak"]

>>> runIdentity (view (cloneLens pathFunction) ((,) <$> ask <*> asks length :: FilePather' (FilePath, Int)) "notes.md")
("notes.md",8)

@since 0.7.0.0
-}
type FilePather :: (Type -> Type) -> Type -> Type
newtype FilePather f a
  = FilePather (FilePath -> f a)
  deriving stock (Generic, Generic1)
  deriving newtype (NFData)
  deriving
    ( Apply
    , Applicative
    , Alternative
    , Plus
    , Monad
    , MonadPlus
    , MonadFail
    , MonadFix
    , MonadZip
    , MonadIO
    , MonadCont
    , MonadReader FilePath
    , Selective
    , Contravariant
    , Divisible
    , Decidable
    )
    via (ReaderT FilePath f)
  deriving (Semigroup, Monoid) via (FilePath -> f a)

deriving via (ReaderT FilePath) instance MonadTrans FilePather

deriving via (ReaderT FilePath f) instance (MonadState s f) => MonadState s (FilePather f)

deriving via (ReaderT FilePath f) instance (MonadWriter w f) => MonadWriter w (FilePather f)

deriving via (ReaderT FilePath f) instance (MonadError e f) => MonadError e (FilePather f)

{- | A 'FilePather' that produces a value without effects.

@since 0.7.0.0
-}
type FilePather' a = FilePather Identity a

-- | @since 0.7.0.0
type FilePatherIO a = FilePather IO a

-- | @since 0.7.0.0
type FilePatherMaybe a = FilePather Maybe a

-- | @since 0.7.0.0
type FilePatherList a = FilePather [] a

-- | @since 0.7.0.0
type FilePatherNonEmpty a = FilePather NonEmpty a

-- | @since 0.7.0.0
type FilePatherEither e a = FilePather (Either e) a

-- | @since 0.7.0.0
type FilePatherMaybeT f a = FilePather (MaybeT f) a

-- | @since 0.7.0.0
type FilePatherExceptT e f a = FilePather (ExceptT e f) a

{- | A 'FilePather' that fails with an exit code, using 'Exitcode'.

@since 0.7.0.0
-}
type FilePatherExitcode e f a = FilePather (Exitcode e f) a

-- | @since 0.7.0.0
type FilePatherExitcode' e a = FilePather (Exitcode e Identity) a

{- | Map over the result of a 'FilePather'. This is 'fmap', and is the
function that the fusion rules match.

>>> runIdentity (view (cloneLens pathFunction) (mapFilePather length ask) "notes.md")
8

@since 0.7.0.0
-}
mapFilePather :: (Functor f) => (a -> b) -> FilePather f a -> FilePather f b
mapFilePather f (FilePather g) =
  FilePather (fmap f . g)
{-# NOINLINE [1] mapFilePather #-}

{- | Filter and map over the result of a 'FilePather'. This is 'mapMaybe', and
is the function that the fusion rules match.

>>> view (cloneLens pathFunction) (mapMaybeFilePather (\x -> if x > 1 then Just (x * 10) else Nothing) (FilePather (\p -> [1, length p]))) "abc"
[30]

@since 0.7.0.0
-}
mapMaybeFilePather :: (Filterable f) => (a -> Maybe b) -> FilePather f a -> FilePather f b
mapMaybeFilePather f (FilePather g) =
  FilePather (mapMaybe f . g)
{-# NOINLINE [1] mapMaybeFilePather #-}

{-# RULES
"mapFilePather/mapFilePather" [~1] forall f g x.
  mapFilePather f (mapFilePather g x) =
    mapFilePather (f . g) x
"mapMaybeFilePather/mapMaybeFilePather" [~1] forall f g x.
  mapMaybeFilePather f (mapMaybeFilePather g x) =
    mapMaybeFilePather (\a -> g a >>= f) x
"mapMaybeFilePather/mapFilePather" [~1] forall f g x.
  mapMaybeFilePather f (mapFilePather g x) =
    mapMaybeFilePather (f . g) x
  #-}

-- | @since 0.7.0.0
instance (Functor f) => Functor (FilePather f) where
  fmap =
    mapFilePather
  {-# INLINE fmap #-}

-- | @since 0.7.0.0
instance (Bind f) => Bind (FilePather f) where
  FilePather g >>- k =
    FilePather (\p -> g p >>- \a -> view (cloneLens pathFunction) (k a) p)
  {-# INLINE (>>-) #-}

-- | @since 0.7.0.0
instance (Alt f) => Alt (FilePather f) where
  FilePather g <!> FilePather h =
    FilePather (\p -> g p <!> h p)
  {-# INLINE (<!>) #-}

-- | @since 0.7.0.0
instance (Filterable f) => Filterable (FilePather f) where
  mapMaybe =
    mapMaybeFilePather
  {-# INLINE mapMaybe #-}

-- | @since 0.7.0.0
instance (Distributive f) => Distributive (FilePather f) where
  distribute w =
    FilePather (\p -> distribute (fmap (\x -> view (cloneLens pathFunction) x p) w))
  {-# INLINE distribute #-}

{- | The representation pairs the file path with the representation of @f@.

@since 0.7.0.0
-}
instance (Representable f) => Representable (FilePather f) where
  type Rep (FilePather f) = (FilePath, Rep f)
  tabulate h =
    FilePather (\p -> tabulate (\r -> h (p, r)))
  {-# INLINE tabulate #-}
  index (FilePather g) (p, r) =
    index (g p) r
  {-# INLINE index #-}

{- | The isomorphism between 'FilePather' and 'ReaderT'.

>>> runIdentity (runReaderT (view filePatherReaderT (asks length)) "notes.md")
8

@since 0.7.0.0
-}
filePatherReaderT :: Iso (FilePather f a) (FilePather g b) (ReaderT FilePath f a) (ReaderT FilePath g b)
filePatherReaderT =
  iso (\(FilePather g) -> ReaderT g) (\(ReaderT g) -> FilePather g)
{-# INLINE filePatherReaderT #-}

{- | The class of 'FilePather', with a pair of type variables for each of
its two parameters.

>>> runIdentity (view (cloneLens (filePather . pathFunction)) (asks length :: FilePather' Int) "notes.md")
8

@since 0.8.0.0
-}
class FilePatherLike p f x x' g g' a a' | x -> g a where
  filePather ::
    Optic p f x x' (FilePather g a) (FilePather g' a')

{- | Identity

@since 0.8.0.0
-}
instance (x' ~ FilePather g' a') => FilePatherLike p f (FilePather g a) x' g g' a a' where
  filePather =
    id
  {-# INLINE filePather #-}

{- | Lens

@since 0.8.0.0
-}
instance (p ~ (->), Functor f, x' ~ FilePather g' a') => PathFunctionLike p f (FilePather g a) x' g g' a a' where
  pathFunction f (FilePather k) =
    fmap FilePather (f k)
  {-# INLINE pathFunction #-}

{- | Rebuild a value using only the name of each field.

Laws, for every file path @p@:

> view (cloneLens pathFunction) (lawNamesFilePather x) p == view (cloneLens pathFunction) x p

@since 0.8.0.0
-}
lawNamesFilePather :: FilePather g a -> FilePather g a
lawNamesFilePather s =
  FilePather (view (cloneLens pathFunction) s)

{- | The classes that a data type holding 'FilePather' inherits.

@since 0.8.0.0
-}
type InheritFilePather s g a =
  ( FilePatherLike (->) (Const ()) s s g g a a
  , PathFunctionLike (->) (Const ()) s s g g a a
  )

{- | Check, where it is applied, that @s@ inherits every name of
'FilePather'.

@since 0.8.0.0
-}
inheritFilePather :: (InheritFilePather s g a) => proxy s -> ()
inheritFilePather _ =
  ()