endo-0.2.0.1: Endomorphism utilities.

Copyright(c) 2014-2015, Peter Trško
LicenseBSD3
Maintainerpeter.trsko@gmail.com
Stabilityexperimental
PortabilityCPP, DeriveDataTypeable, DeriveGeneric, FlexibleInstances, NoImplicitPrelude, TypeFamilies
Safe HaskellSafe-Inferred
LanguageHaskell2010

Data.Monoid.Endo.Fold

Contents

Description

Generic folding for various endomorphism representations.

Synopsis

Usage Examples

Examples in this section were taken from real live production code, but they were tamed down a little.

Basic Idea

Lets define simple application Config data type as:

data Verbosity = Silent | Normal | Verbose | Annoying
  deriving (Bounded, Data, Enum, Eq, Ord, Show, Typeable)

data Config = Config
    { _verbosity :: Verbosity
    , _outputFile :: FilePath
    }
  deriving (Show)

Now lets define setters for _verbosity and _outputFile:

setVerbosity :: Verbosity -> E Config
setVerbosity b cfg = cfg{_verbosity = b}

setOutputFile :: FilePath -> E Config
setOutputFile b cfg = cfg{_outputFile = b}

Note that E is defined in Data.Monoid.Endo module and it looks like:

type E a = a -> a

Its purpose is to simplify type signatures.

Now lets get to our first example:

example1 :: E Config
example1 = appEndo $ foldEndo
    &$ setVerbosity Annoying
    &$ setOutputFile "an.out.put"

Above example shows us that it is possible to modify Config as if it was a monoid, but without actually having to state it as such. In practice it is not always possible to define it as Monoid, or at least as a Semigroup. Endomorphism are monoids under composition, therefore they are what usually works in situations when the modified data type can not be instantiated as a monoid.

Working With Corner Cases

In real applications corner cases arise quite easily, e.g. FilePath has one pathological case, and that is "". There is a lot of ways to handle it. Here we will concentrate only few basic techniques to illustrate versatility of our approach.

-- | Trying to set output file to "" will result in keeping original
-- value.
setOutputFile2 :: FilePath -> E Config
setOutputFile2 "" = id
setOutputFile2 fp = setOutputFile fp

example2 :: E Config
example2 = appEndo $ foldEndo
    &$ setVerbosity Annoying
    &$ setOutputFile2 "an.out.put"

Same as above, but exploits instance AnEndo a => AnEndo Maybe a:

setOutputFile3 :: FilePath -> Maybe (E Config)
setOutputFile3 "" = Nothing
setOutputFile3 fp = Just $ setOutputFile fp

example3 :: E Config
example3 = appEndo $ foldEndo
    &$ setVerbosity Annoying
    &$ setOutputFile3 "an.out.put"

Great thing about Maybe is the fact that it has Alternative and MonadPlus instances. Using guard may simplify setOutputFile3 in to definition like following:

setOutputFile3':: FilePath -> Maybe (E Config)
setOutputFile3' fp = setOutputFile fp <$ guard (not (null fp))

Following example uses common pattern of using Either as error reporting monad. This approach can be easily modified for arbitrary error reporting monad.

setOutputFile4 :: FilePath -> Either String (E Config)
setOutputFile4 "" = Left "Output file: Empty file path."
setOutputFile4 fp = Right $ setOutputFile fp

example4 :: Either String (E Config)
example4 = appEndo <&$> foldEndo
    <*> pure (setVerbosity Annoying)
    <*> setOutputFile4 "an.out.put"

Notice, that above example uses applicative style. Normally, when using this style for setting record values, one needs to keep in sync the order of constructor arguments, and order of operations. Using foldEndo (and its dual dualFoldEndo) doesn't have this restriction.

Using With Lenses

Instead of setter functions one may want to use lenses. In this example we use types from lens package, but definitions use function from between package:

verbosity :: Lens' Config Verbosity
verbosity = _verbosity ~@@^> \s b -> s{_verbosity = b}

outputFile :: Lens' Config FilePath
outputFile = _outputFile ~@@^> \s b -> s{_outputFile = b}

Now setting values of Config would look like:

example5 :: E Config
example5 = appEndo $ foldEndo
    &$ verbosity  .~ Annoying
    &$ outputFile .~ "an.out.put"

Other Usage

Probably one of the most interesting things that can be done with this module is following:

instance AnEndo Verbosity where
    type EndoOperatesOn Verbosity = Config
    anEndo = Endo . set verbosity

newtype OutputFile = OutputFile FilePath

instance AnEndo OutputFile where
    type EndoOperatesOn OutputFile = Config
    anEndo (OutputFile fp) = Endo $ outputFile .~ fp

example6 :: E Config
example6 = appEndo $ foldEndo
    &$ Annoying
    &$ OutputFile "an.out.put"

Using With optparse-applicative

This is a more complex example that defines parser for optparse-applicative built on top of some of the above definitions:

options :: Parser Config
options = runIdentityT $ runEndo defaultConfig <$> options'
  where
    -- All this IdentityT clutter is here to avoid orphan instances.
    options' :: IdentityT Parser (Endo Config)
    options' = foldEndo
        <*> outputOption     -- :: IdentityT Parser (Maybe (E Config))
        <*> verbosityOption  -- :: IdentityT Parser (Maybe (E Config))
        <*> annoyingFlag     -- :: IdentityT Parser (E Config)
        <*> silentFlag       -- :: IdentityT Parser (E Config)
        <*> verboseFlag      -- :: IdentityT Parser (E Config)

    defaultConfig :: Config
    defaultConfig = Config Normal ""

main :: IO ()
main = execParser (info options fullDesc) >>= print

Example of running above main function:

>>> :main -o an.out.put --annoying
Config {_verbosity = Annoying, _outputFile = "an.out.put"}

Parsers for individual options and flags are wrapped in IdentityT, because there is no following instance:

instance FoldEndoArgs r => FoldEndoArgs (Parser r)

But there is:

instance (Applicative f, FoldEndoArgs r) => FoldEndoArgs (IdentityT f r)

Functions used by the above code example:

outputOption :: IdentityT Parser (Maybe (E Config))
outputOption =
    IdentityT . optional . option (set outputFile <$> parseFilePath)
    $ short 'o' <> long "output" <> metavar "FILE"
        <> help "Store output in to a FILE."
  where
    parseFilePath = eitherReader $ \s ->
        if null s
            then Left "Option argument can not be empty file path."
            else Right s

verbosityOption :: IdentityT Parser (Maybe (E Config))
verbosityOption =
    IdentityT . optional . option (set verbosity <$> parseVerbosity)
    $ long "verbosity" <> metavar "LEVEL" <> help "Set verbosity to LEVEL."
  where
    verbosityToStr = map toLower . Data.showConstr . Data.toConstr
    verbosityIntValues = [(show $ fromEnum v, v) | v <- [Silent .. Annoying]]
    verbosityStrValues =
        ("default", Normal) : [(verbosityToStr v, v) | v <- [Silent .. Annoying]]

    parseVerbosityError = unwords
        [ "Verbosity can be only number from interval"
        , show $ map fromEnum [minBound, maxBound :: Verbosity]
        , "or one of the following:"
        , concat . intersperse ", " $ map fst verbosityStrValues
        ]

    parseVerbosity = eitherReader $ s ->
        case lookup s $ verbosityIntValues ++ verbosityStrValues of
            Just v  -> Right v
            Nothing -> Left parseVerbosityError

annoyingFlag :: IdentityT Parser (E Config)
annoyingFlag = IdentityT . flag id (verbosity .~ Annoying)
    $ long "annoying" <> help "Set verbosity to maximum."

silentFlag :: IdentityT Parser (E Config)
silentFlag = IdentityT . flag id (verbosity .~ Silent)
    $ short s <> long "silent" <> help "Set verbosity to minimum."

verboseFlag :: IdentityT Parser (E Config)
verboseFlag = IdentityT . flag id (verbosity .~ Verbose)
    $ short v <> long "verbose" <> help "Be verbose."

Generic Endomorphism Folding

foldEndo :: FoldEndoArgs args => args Source

Fold all variously represented endomorphisms in to one endomorphism.

Order in which endomorphisms are folded is preserved:

>>> foldEndo (Endo (1:)) [(2:), (3:)] `appEndo` []
[1,2,3]

For numbers it would look like:

>>> foldEndo (Endo (+1)) [(+2), (*3)] `appEndo` 1
6

Above can be seen as:

>>> (+1) . (+2) . (*3) $ 1
6

dualFoldEndo :: FoldEndoArgs args => args Source

Same as foldEndo, but folds endomorphisms in reverse order.

Following are the same examples as for foldEndo function. Please, note the differences in results.

Order in which endomorphisms are folded is reversed:

>>> dualFoldEndo (Endo (1:)) [(2:), (3:)] `appEndo` []
[2,3,1]

For numbers it would look like:

>>> dualFoldEndo (Endo (+1)) [(+2), (*3)] `appEndo` 1
12

Above can be seen as:

>>> (*3) . (+2) . (+1) $ 1
12

Type Classes

class FoldEndoArgs a where Source

Class of arguments for foldEndo and its dual dualFoldEndo functions.

Note that results are instances of this (FoldEndoArgs) class and endomorphism representations are instances of AnEndo type class.

Associated Types

type ResultOperatesOn a Source

Extracts type of a value that is modified by the result.

type Result a Source

Result type of the whole endomorphism folding. It can be used to restrict the result of foldEndo and dualFoldEndoArgs. Example:

-- Type restricted version of foldEndo that forces the result of the
-- whole folding machinery to be "Endo Int".
myFoldEndo
    :: (Result args ~ Endo Int, FoldEndoArgs args)
    => args -> args
myFoldEndo = foldEndo

Instances

FoldEndoArgs r => FoldEndoArgs (IO r)

Allows endomorphism folding for endomorphisms wrapped inside IO monad. Examples:

foldEndo <*> ((++) <$> getLine) <*> ((++) <$> getLine)
    :: :: (FoldEndoArgs r, ResultOperatesOn r ~ String) => IO r

In the next example, prefix ghci> indicates GHCi prompt, ghci| is GHCi continuation prompt, <<< indicates user input and >>> GHCi output. Also, :{ and :} is GHCi's way of starting and ending multiline mode, respectively.

ghci> :{
ghci| runEndo "" <&$> foldEndo
ghci|     <*> ((++) <$> getLine)
ghci|     <*> ((++) <$> getLine)
ghci| :}
<<< alpha
<<< bet
>>> "alphabet"
FoldEndoArgs (Endo a) 
FoldEndoArgs r => FoldEndoArgs (Maybe r) 
FoldEndoArgs r => FoldEndoArgs (Identity r) 
(AnEndo a, FoldEndoArgs r, (~) * (EndoOperatesOn a) (ResultOperatesOn r)) => FoldEndoArgs (a -> r)

Recurse along FoldEndoArgs instances if first argument is AnEndo. This instance is actually what makes foldEndo and dualFoldEndo variadic-like.

FoldEndoArgs r => FoldEndoArgs (Either e r) 
(Monoid c, FoldEndoArgs r) => FoldEndoArgs (Const c r)

This basically discards result of folding, in example:

>>> foldEndo ('n':) ('o':) :: Const () (Endo String)
Const ()
(Monad m, Functor m, FoldEndoArgs r) => FoldEndoArgs (MaybeT m r) 
(Applicative f, FoldEndoArgs r) => FoldEndoArgs (ListT f r) 
(Applicative f, FoldEndoArgs r) => FoldEndoArgs (IdentityT f r)

This instance can be used in cases when there is no FoldEndoArgs instance for a specific Applicative functor. Example:

runIdentityT $ foldEndo
    <*> IdentityT parseSomething
    <*> IdentityT parseSomethingElse
(Applicative f, FoldEndoArgs r, Monoid w) => FoldEndoArgs (WriterT w f r) 
(Applicative f, FoldEndoArgs r, Monoid w) => FoldEndoArgs (WriterT w f r) 
(Monad m, FoldEndoArgs r) => FoldEndoArgs (StateT s m r) 
(Monad m, FoldEndoArgs r) => FoldEndoArgs (StateT s m r) 
(Applicative f, FoldEndoArgs r) => FoldEndoArgs (ReaderT r' f r) 
(Applicative f, Applicative g, FoldEndoArgs r) => FoldEndoArgs (Compose f g r) 
(Applicative f, Applicative g, FoldEndoArgs r) => FoldEndoArgs (Product f g r) 
(Monad m, Functor m, Monoid w, FoldEndoArgs r) => FoldEndoArgs (RWST r' w s m r) 
(Monad m, Functor m, Monoid w, FoldEndoArgs r) => FoldEndoArgs (RWST r' w s m r) 

class AnEndo a where Source

Class that represents various endomorphism representation. In other words anything that encodes a -> a can be instance of this class.

Here are some important instances with not so obvious definitions.

instance AnEndo (Proxy a) where
    type EndoOperatesOn (Proxy a) = a

    anEndo    _ = mempty -- = Endo id
    aDualEndo _ = mempty

It got quite common to use Proxy data type as an explicit way to pass types around. Above instance allows you to restrict type of result of endomorphism folding, to some extent.

instance AnEndo a => AnEndo (Maybe a) where
    type EndoOperatesOn (Maybe a) = EndoOperatesOn a

    anEndo Nothing  = mempty -- = Endo id
    anEndo (Just e) = anEndo e

    -- Definition of aDualEndo is analogous.

Instance for Maybe lets us conditionally inject endomorphism in to a folding chain.

Minimal complete definition

anEndo | aDualEndo

Associated Types

type EndoOperatesOn a Source

Extract type on which endomorphism operates, e.g. for Endo a it would be a.

Methods

anEndo :: a -> Endo (EndoOperatesOn a) Source

Convert value encoding a -> a in to Endo. Default implementation:

anEndo = getDual . aDualEndo

aDualEndo :: a -> Dual (Endo (EndoOperatesOn a)) Source

Dual to anEndo. Default implementation:

aDualEndo = Dual . anEndo

Instances

AnEndo a => AnEndo [a] 
AnEndo (Endo a) 
AnEndo a => AnEndo (Maybe a) 
AnEndo (a -> a) 
(AnEndo a, AnEndo b, (~) * (EndoOperatesOn a) (EndoOperatesOn b)) => AnEndo (a, b) 
AnEndo (Proxy * a)

Constructs identity endomorphism for specified phantom type.

(Foldable f, AnEndo a) => AnEndo (Reverse f a)

Fold in reverese order.

(Foldable f, AnEndo a) => AnEndo (WrappedFoldable f a) 
(AnEndo a, AnEndo b, AnEndo c, (~) * (EndoOperatesOn a) (EndoOperatesOn b), (~) * (EndoOperatesOn a) (EndoOperatesOn c)) => AnEndo (a, b, c) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4)) => AnEndo (a1, a2, a3, a4) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, AnEndo a5, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4), (~) * (EndoOperatesOn a1) (EndoOperatesOn a5)) => AnEndo (a1, a2, a3, a4, a5) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, AnEndo a5, AnEndo a6, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4), (~) * (EndoOperatesOn a1) (EndoOperatesOn a5), (~) * (EndoOperatesOn a1) (EndoOperatesOn a6)) => AnEndo (a1, a2, a3, a4, a5, a6) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, AnEndo a5, AnEndo a6, AnEndo a7, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4), (~) * (EndoOperatesOn a1) (EndoOperatesOn a5), (~) * (EndoOperatesOn a1) (EndoOperatesOn a6), (~) * (EndoOperatesOn a1) (EndoOperatesOn a7)) => AnEndo (a1, a2, a3, a4, a5, a6, a7) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, AnEndo a5, AnEndo a6, AnEndo a7, AnEndo a8, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4), (~) * (EndoOperatesOn a1) (EndoOperatesOn a5), (~) * (EndoOperatesOn a1) (EndoOperatesOn a6), (~) * (EndoOperatesOn a1) (EndoOperatesOn a7), (~) * (EndoOperatesOn a1) (EndoOperatesOn a8)) => AnEndo (a1, a2, a3, a4, a5, a6, a7, a8) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, AnEndo a5, AnEndo a6, AnEndo a7, AnEndo a8, AnEndo a9, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4), (~) * (EndoOperatesOn a1) (EndoOperatesOn a5), (~) * (EndoOperatesOn a1) (EndoOperatesOn a6), (~) * (EndoOperatesOn a1) (EndoOperatesOn a7), (~) * (EndoOperatesOn a1) (EndoOperatesOn a8), (~) * (EndoOperatesOn a1) (EndoOperatesOn a9)) => AnEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9) 
(AnEndo a1, AnEndo a2, AnEndo a3, AnEndo a4, AnEndo a5, AnEndo a6, AnEndo a7, AnEndo a8, AnEndo a9, AnEndo a10, (~) * (EndoOperatesOn a1) (EndoOperatesOn a2), (~) * (EndoOperatesOn a1) (EndoOperatesOn a3), (~) * (EndoOperatesOn a1) (EndoOperatesOn a4), (~) * (EndoOperatesOn a1) (EndoOperatesOn a5), (~) * (EndoOperatesOn a1) (EndoOperatesOn a6), (~) * (EndoOperatesOn a1) (EndoOperatesOn a7), (~) * (EndoOperatesOn a1) (EndoOperatesOn a8), (~) * (EndoOperatesOn a1) (EndoOperatesOn a9), (~) * (EndoOperatesOn a1) (EndoOperatesOn a10)) => AnEndo (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) 

Type Wrappers

newtype WrappedFoldable f a Source

Wrapper for Foldable instances.

This allows using foldEndo and dualFoldEndo for any Foldable instance without the need for OverlappingInstances language extension.

Constructors

WrapFoldable 

Fields

getFoldable :: f a
 

Instances

(Data (f a), Typeable (* -> *) f, Typeable * a) => Data (WrappedFoldable f a) 
Read (f a) => Read (WrappedFoldable f a) 
Show (f a) => Show (WrappedFoldable f a) 
Generic (WrappedFoldable f a) 
(Foldable f, AnEndo a) => AnEndo (WrappedFoldable f a) 
Typeable ((* -> *) -> * -> *) WrappedFoldable 
type Rep (WrappedFoldable f a) 
type EndoOperatesOn (WrappedFoldable f a) = EndoOperatesOn a 

Utility Functions

(&$) :: (a -> b) -> a -> b infixl 1 Source

Variant of function ($) :: (a -> b) -> a -> b, from Data.Function module, but with fixity as (&) :: a -> (a -> b) -> b function from Data.Function module (available in base since version 4.8.0.0).

(<&$>) :: Functor f => (a -> b) -> f a -> f b infixl 1 Source

Variant of function (<$>) :: Functor f => (a -> b) -> a -> b from Data.Functor module, but with fixity as &$ function.

embedEndoWith Source

Arguments

:: (AnEndo e, EndoOperatesOn e ~ a) 
=> (Endo a -> b)

Embedding function.

-> e 
-> b 

Use Endo (possibly result of foldEndo) and use it to create value of different type.

Examples:

embedEndoWith tell
    :: (Monad m, AnEndo e, w ~ EndoOperatesOn e)
    => e
    -> WriterT (Endo w) m ()

embedEndoWith modify
    :: (Monad m, AnEndo e, s ~ EndoOperatesOn e)
    => e
    -> StateT s m ()

See also embedDualEndoWith.

embedDualEndoWith Source

Arguments

:: (AnEndo e, EndoOperatesOn e ~ a) 
=> (Dual (Endo a) -> b)

Embedding function.

-> e 
-> b 

Dual to embedEndoWith, which uses aDualEndo instead of anEndo.