/endo

Endomorphism utilities.

Primary LanguageHaskellBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

Endo

Hackage Hackage Dependencies Haskell Programming Language BSD3 License

Build

Description

Endomorphism utilities.

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 (Show)

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 order of constructor arguments and order of operations. Using foldEndo (and its dual dualFoldEndo) doesn't have this restriction.

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 -o an.out.put --annoying
-- Config {_verbosity = Annoying, _outputFile = "an.out.put"}
main :: IO ()
main = execParser (info options fullDesc) >>= print

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."

Building Options

  • -fpedantic (disabled by default)

    Pass additional warning flags to GHC.

License

The BSD 3-Clause License, see LICENSE file for details.

Contributions

Contributions, pull requests and bug reports are welcome! Please don't be afraid to contact author using GitHub or by e-mail.