endo

Endomorphism utilities. https://github.com/trskop/endo

Latest on Hackage:0.3.0.1

This package is not currently in any snapshots. If you're interested in using it, we recommend adding it to Stackage Nightly. Doing so will make builds more reliable, and allow stackage.org to host generated Haddocks.

BSD3 licensed by Peter Trško
Maintained by peter.trsko@gmail.com

Endo

[Hackage][Hackage: endo] Hackage Dependencies [Haskell Programming Language][Haskell.org] [BSD3 License][tl;dr Legal: BSD3]

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:

Haskell 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:

Haskell 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:

Haskell type E a = a -> a

Its purpose is to simplify type signatures.

Now lets get to our first example:

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

Haskell -- | 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:

Haskell 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:

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

Haskell 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][Hackage: lens], but definitions use function from between package:

Haskell 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:

Haskell 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:

Haskell 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][Hackage: optparse-applicative] built on top of some of the above definitions:

Haskell 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:

Haskell instance FoldEndoArgs r => FoldEndoArgs (Parser r)

But there is:

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

Functions used by the above code example:

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

http://hackage.haskell.org/package/between "between package on Hackage" [Hackage: endo]: http://hackage.haskell.org/package/endo "endo package on Hackage" [Hackage: lens]: http://hackage.haskell.org/package/lens "lens package on Hackage" [Hackage: optparse-applicative]: http://hackage.haskell.org/package/optparse-applicative "optparse-applicative package on Hackage" [Haskell.org]: http://www.haskell.org "The Haskell Programming Language" [LICENSE]: https://github.com/trskop/endo/blob/master/LICENSE "License of endo package." [tl;dr Legal: BSD3]: https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29 "BSD 3-Clause License (Revised)"

Changes

ChangeLog / ReleaseNotes

Version 0.3.0.1

Version 0.3.0.0

  • Introducing type :-> to simplify type signatures of endomorphism folding functions that restrict type of a result. (new)
  • Type class AnEndo moved in to a separate module Data.Monoid.Endo.AnEndo. Definitions are reexported by Data.Monoid.Endo.Fold, therefore providing backward compatible API. (change)
  • Introducing instance AnEndo a => AnEndo (Identity a). (new)
  • Introducing FromEndo type class for conversion of endomorphism in to a value. It is a dual to AnEndo type class. This type class resides in its own module Data.Monoid.Endo.FromEndo. (new)
  • Introducing ApplyEndo newtype that provides easier endomorphism evaluation in cases when there is an "obvious" default value. This type has its own module Data.Monoid.Endo.Apply that also provides various helper functions and type class instances. (new)
  • Providing Eq1, Ord1, Read1 and Show1 instances if built with [transformers package][transformers] >=0.5 or base >=4.9 (i.e. GHC >=8.0.1) is available. (new)
  • Providing Generic1 instance for WrappedFoldable. (new)
  • Introducing instance AnEndo a => AnEndo (Option a), but only when compiled with base >=4.9, since that is the first version of base which contains Semigroup. (new)
  • Bumped upper bound of [transformers package][transformers] to include 0.5.* versions. (change)
  • Synchronized API documentation of Data.Monoid.Endo.Fold with README. (trivial change)
  • Uploaded to Hackage: http://hackage.haskell.org/package/endo-0.3.0.0

Version 0.2.0.1

Version 0.2.0.0

  • Default implementation for anEndo method of 'AnEndo' type class, which is now defined as: anEndo = getDual . aDualEndo. As a consequence it is now possible to define complete instances of AnEndo by providing either anEndo or aDualEndo. (new, change)
  • Introducing associated type Result to FoldEndoArgs type class. This allows result of the whole folding to be explicitly stated in a type signature. (new, change)
  • Introducing functions embedEndoWith and embedDualEndoWith. Both can be used to simplify application of endomorphisms that are result of folding. (new) - embedEndoWith :: (AnEndo e, EndoOperatesOn e ~ a) => (Endo a -> b) -> e -> b - embedDualEndoWith :: (AnEndo e, EndoOperatesOn e ~ a) => (Dual (Endo a) -> b) -> e -> b
  • Introducing instance AnEndo (Proxy a), which is useful in cases when one needs to force constraint EndoOperatesOn args ~ a where a is the a from Proxy a. This is done by encoding value of Proxy in to identity endomorphism that operates on specified type a. (new)
  • Introducing instance (Monoid c, FoldEndoArgs r) => FoldEndoArgs (Const c r), which is useful in cases when one needs to discard the computation and return a constant instead. (new)
  • Bumping upper bounds on base and between, therefore it now builds on GHC 7.10 with base 4.8. (new)
  • Uploaded to Hackage: http://hackage.haskell.org/package/endo-0.2.0.0

Version 0.1.0.2

Version 0.1.0.1

Version 0.1.0.0

http://hackage.haskell.org/ "HackageDB (or just Hackage) is a collection of releases of Haskell packages." [transformers]: https://hackage.haskell.org/package/transformers "Package transformers on Hackage."

comments powered byDisqus