{-# LANGUAGE NoImplicitPrelude #-} -- | -- Module: $HEADER$ -- Description: Function combinator "between" and its variations. -- Copyright: (c) 2013, 2014 Peter Trsko -- License: BSD3 -- -- Maintainer: peter.trsko@gmail.com -- Stability: experimental -- Portability: portable -- -- During development it is common occurrence to modify deeply nested -- structures. One of the best known libraries for this purpose is -- <http://hackage.haskell.org/package/lens lens>, but it's quite -- overkill for some purposes. -- -- This module describes simple and composable combinators that are built on -- top of very basic concept: -- -- @f '.' h '.' g@ -- -- Where @f@ and @g@ are fixed. It is possible to reduce it to just: -- -- @(f '.') '.' ('.' g)@ -- -- Which is the core pattern used by all functions defined in this module. -- -- Trying to generalize this pattern further ends as: -- @(f 'Data.Functor.<$>') '.' ('Data.Functor.<$>' g)@, where -- @'Data.Functor.<$>' = 'fmap'@. Other combinations of substituting '.' for -- 'fmap' will end up less or equally generic. Type of such expression is: -- -- > \f g -> (f <$>) . (<$> g) -- > :: Functor f => (b -> c) -> f a -> (a -> b) -> f c -- -- Which doesn't give us much more power. Instead of going for such -- generalization we kept the original @((f '.') '.' ('.' g))@ which we named -- 'between' or '~@~' in its infix form. module Data.Function.Between ( -- * Composability -- -- $composability -- * Mapping Functions For Newtypes -- -- $mappingFunctionsForNewtypes -- * Constructing Lenses -- -- $lenses -- * Between Function Combinator -- -- | Captures common pattern of @\\g -> (f '.' g '.' h)@ where @f@ and @h@ -- are fixed parameters. between , (~@~) , (~@@~) -- * Derived Combinators -- -- | Combinators that either further parametrise @f@ or @g@ in -- @f '.' g '.' h@, or apply '~@~' more then once. , (^@~) , (~@@^) , (^@^) , (^@@^) , between2l , between3l -- ** Lifted Combinators -- -- | Combinators based on '~@~', '^@~', '^@^', and their flipped variants, -- that use 'fmap' to lift one or more of its arguments to operate in -- 'Functor' context. , (<~@~>) , (<~@@~>) , (<~@~) , (~@@~>) , (~@~>) , (<~@@~) , (<^@~) , (~@@^>) , (<^@^>) , (<^@@^>) , (<^@^) , (^@@^>) , (^@^>) , (<^@@^) ) where import Data.Functor (Functor(fmap)) import Data.Function ((.), flip) -- | Core combinator of this module and we build others on top of. It also has -- an infix form '~@~' and flipped infix form '~@@~'. -- -- This function Defined as: -- -- @ -- 'between' f g -> (f .) . (. g) -- @ between :: (c -> d) -> (a -> b) -> (b -> c) -> a -> d between f g = (f .) . (. g) -- | Infix variant of 'between'. -- -- Fixity is left associative and set to value 8, which is one less then fixity -- of function composition ('.'). (~@~) :: (c -> d) -> (a -> b) -> (b -> c) -> a -> d (~@~) = between infixl 8 ~@~ -- | Flipped variant of '~@~', i.e. flipped infix variant of 'between'. -- -- Fixity is right associative and set to value 8, which is one less then -- fixity of function composition ('.'). (~@@~) :: (a -> b) -> (c -> d) -> (b -> c) -> a -> d (~@@~) = flip between infixr 8 ~@@~ -- | As '~@~', but first function is also parametrised with @a@, hence the name -- '^@~'. Character @^@ indicates which argument is parametrised with -- additional argument. -- -- This function is defined as: -- -- @ -- (f '^@~' g) h a -> (f a '~@~' g) h a -- @ -- -- Fixity is left associative and set to value 8, which is one less then -- fixity of function composition ('.'). (^@~) :: (a -> c -> d) -> (a -> b) -> (b -> c) -> a -> d (f ^@~ g) h a = (f a `between` g) h a infixl 8 ^@~ -- | Flipped variant of '^@~'. -- -- Fixity is right associative and set to value 8, which is one less then -- fixity of function composition ('.'). (~@@^) :: (a -> b) -> (a -> c -> d) -> (b -> c) -> a -> d (~@@^) = flip (^@~) infixr 8 ~@@^ -- | Pass additional argument to first two function arguments. -- -- This function is defined as: -- -- @ -- (f '^@^' g) h a b -> (f a '~@~' g a) h b -- @ -- -- See also '^@~' to note the difference, most importantly that '^@~' passes -- the same argument to all its functional arguments. Function '^@~' can be -- defined in terms of this one as: -- -- @ -- (f '^@~' g) h a = (f '^@^' 'Data.Function.const' g) h a a -- @ -- -- We can do it also the other way around and define '^@^' using '^@~': -- -- @ -- f '^@^' g = -- 'Data.Tuple.curry' . (f . 'Data.Tuple.snd' '^@~' 'Data.Tuple.uncurry' g) -- @ -- -- Fixity is set to value 8, which is one less then of function composition -- ('.'). (^@^) :: (a -> d -> e) -> (a -> b -> c) -> (c -> d) -> a -> b -> e (f ^@^ g) h a = (f a `between` g a) h infix 8 ^@^ -- | Flipped variant of '^@^'. -- -- Fixity is set to value 8, which is one less then of function composition -- ('.'). (^@@^) :: (a -> b -> c) -> (a -> d -> e) -> (c -> d) -> a -> b -> e (^@@^) = flip (^@^) infix 8 ^@@^ -- | Apply function @g@ to each argument of binary function and @f@ to its -- result. In suffix \"2l\" the number is equal to arity of the function it -- accepts as a third argument and character \"l\" is for \"left associative\". -- -- @ -- 'between2l' f g = (f '~@~' g) '~@~' g -- @ -- -- Interesting observation: -- -- @ -- (\\f g -> 'between2l' 'Data.Function.id' g f) === 'Data.Function.on' -- @ between2l :: (c -> d) -> (a -> b) -> (b -> b -> c) -> a -> a -> d between2l f g = (f `between` g) `between` g -- | Apply function @g@ to each argument of ternary function and @f@ to its -- result. In suffix \"3l\" the number is equal to arity of the function it -- accepts as a third argument and character \"l\" is for \"left associative\". -- -- This function is defined as: -- -- @ -- 'between3l' f g = ((f '~@~' g) '~@~' g) '~@~' g -- @ -- -- Alternatively it can be defined using 'between2l': -- -- @ -- 'between3l' f g = 'between2l' f g '~@~' g -- @ between3l :: (c -> d) -> (a -> b) -> (b -> b -> b -> c) -> a -> a -> a -> d between3l f g = ((f `between` g) `between` g) `between` g -- | Convenience wrapper for: -- -- @ -- \\f g -> 'fmap' f '~@~' 'fmap' g -- @ -- -- Name of '<~@~>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- both its arguments and then we apply '~@~'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (<~@~>) :: (Functor f, Functor g) => (c -> d) -> (a -> b) -> (f b -> g c) -> f a -> g d f <~@~> g = fmap f `between` fmap g infix 8 <~@~> -- | Flipped variant of '<~@~>'. -- -- Name of '<~@@~>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- both its arguments and then we apply '~@@~'. -- -- Fixity is set to value 8, which is one less then of function composition -- ('.'). (<~@@~>) :: (Functor f, Functor g) => (a -> b) -> (c -> d) -> (f b -> g c) -> f a -> g d f <~@@~> g = fmap g `between` fmap f infix 8 <~@@~> -- | Apply fmap to first argument of '~@~'. Dual to '~@~>' which applies -- 'fmap' to second argument. -- -- Defined as: -- -- @ -- f '<~@~' g = 'fmap' f '~@~' g -- @ -- -- This function allows us to define lenses mostly for pair of functions that -- form an isomorphism. See section <#g:3 Constructing Lenses> for details. -- -- Name of '<~@~' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- first (left) argument and then we apply '~@~'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (<~@~) :: Functor f => (c -> d) -> (a -> b) -> (b -> f c) -> a -> f d (<~@~) = between . fmap infixl 8 <~@~ -- | Flipped variant of '<~@~'. -- -- This function allows us to define lenses mostly for pair of functions that -- form an isomorphism. See section <#g:3 Constructing Lenses> for details. -- -- Name of '~@@~>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- second (right) argument and then we apply '~@@~'. -- -- Fixity is right associative and set to value 8, which is one less then -- fixity of function composition ('.'). (~@@~>) :: Functor f => (a -> b) -> (c -> d) -> (b -> f c) -> a -> f d (~@@~>) = flip (<~@~) infixr 8 ~@@~> -- | Apply fmap to second argument of '~@~'. Dual to '<~@~' which applies -- 'fmap' to first argument. -- -- Defined as: -- -- @ -- f '~@~>' g -> f '~@~' 'fmap' g -- @ -- -- Name of '~@~>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- second (right) argument and then we apply '~@~'. -- -- Fixity is right associative and set to value 8, which is one less then -- of function composition ('.'). (~@~>) :: Functor f => (c -> d) -> (a -> b) -> (f b -> c) -> f a -> d (~@~>) f = between f . fmap infixl 8 ~@~> -- | Flipped variant of '~@~>'. -- -- Name of '<~@@~' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- first (left) argument and then we apply '~@@~'. -- -- Fixity is left associative and set to value 8, which is one less then -- fixity of function composition ('.'). (<~@@~) :: Functor f => (a -> b) -> (c -> d) -> (f b -> c) -> f a -> d (<~@@~) = flip (~@~>) infixr 8 <~@@~ -- | Convenience wrapper for: @\\f g -> 'fmap' . f '^@~' g@. -- -- This function has the same functionality as function -- -- @ -- lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b -- @ -- -- Which is defined in <http://hackage.haskell.org/package/lens lens package>. -- Only difference is that arguments of '<^@~' are flipped. See also section -- <#g:3 Constructing Lenses>. -- -- Name of '<^@~' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- first (left) arguments and then we apply '^@~'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (<^@~) :: Functor f => (a -> c -> d) -> (a -> b) -> (b -> f c) -> a -> f d (<^@~) f = (fmap . f ^@~) infixl 8 <^@~ -- | Flipped variant of '~@^>'. -- -- This function has the same functionality as function -- -- @ -- lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b -- @ -- -- Which is defined in <http://hackage.haskell.org/package/lens lens package>. -- See also section <#g:3 Constructing Lenses>. -- -- Name of '~@^>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- second (right) arguments and then we apply '~@^>'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (~@@^>) :: Functor f => (a -> b) -> (a -> c -> d) -> (b -> f c) -> a -> f d (~@@^>) = flip (<^@~) infixl 8 ~@@^> -- | Convenience wrapper for: @\\f g -> 'fmap' . f '^@^' 'fmap' . g@. -- -- Name of '<^@^>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- both its arguments and then we apply '^@^'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (<^@^>) :: (Functor f, Functor g) => (a -> d -> e) -> (a -> b -> c) -> (f c -> g d) -> a -> f b -> g e (f <^@^> g) h a = (fmap (f a) `between` fmap (g a)) h infix 8 <^@^> -- | Flipped variant of '<^@^>'. -- -- Name of '<^@@^>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- both its arguments and then we apply '^@@^'. -- -- Fixity is set to value 8, which is one less then of function composition -- ('.'). (<^@@^>) :: (Functor f, Functor g) => (a -> b -> c) -> (a -> d -> e) -> (f c -> g d) -> a -> f b -> g e (<^@@^>) = flip (<^@^>) infix 8 <^@@^> -- | Convenience wrapper for: @\\f g -> 'fmap' . f '^@^' g@. -- -- This function allows us to define generic lenses from gettern and setter. -- See section <#g:3 Constructing Lenses> for details. -- -- Name of '<^@^' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- first (left) arguments and then we apply '^@^'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (<^@^) :: Functor f => (a -> d -> e) -> (a -> b -> c) -> (c -> f d) -> a -> b -> f e (f <^@^ g) h a = (fmap (f a) `between` g a) h infix 8 <^@^ -- | Flipped variant of '<^@^'. -- -- This function allows us to define generic lenses from gettern and setter. -- See section <#g:3 Constructing Lenses> for details. -- -- Name of '^@@^>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- second (right) arguments and then we apply '^@@^'. -- -- Fixity is set to value 8, which is one less then of function composition -- ('.'). (^@@^>) :: Functor f => (a -> b -> c) -> (a -> d -> e) -> (c -> f d) -> a -> b -> f e (^@@^>) = flip (<^@^) infix 8 ^@@^> -- | Convenience wrapper for: @\\f g -> f '^@^' 'fmap' . g@. -- -- Name of '^@^>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- second (right) arguments and then we apply '^@^'. -- -- Fixity is left associative and set to value 8, which is one less then -- of function composition ('.'). (^@^>) :: Functor f => (a -> d -> e) -> (a -> b -> c) -> (f c -> d) -> a -> f b -> e (f ^@^> g) h a = (f a `between` fmap (g a)) h infix 8 ^@^> -- | Flipped variant of '<^@^>'. -- -- Name of '<^@@^>' simply says that we apply 'Data.Functor.<$>' ('fmap') to -- first (left) arguments and then we apply '^@@^'. -- -- Fixity is set to value 8, which is one less then of function composition -- ('.'). (<^@@^) :: Functor f => (a -> b -> c) -> (a -> d -> e) -> (f c -> d) -> a -> f b -> e (<^@@^) = flip (^@^>) infix 8 <^@@^ -- $composability -- -- > (f . h) ~@~ (i . g) === (f ~@~ g) . (h ~@~ i) -- -- This shows us that it is possible to define @(f '~@~' g)@ and @(h '~@~' i)@ -- separately, for reusability, and then compose them. -- -- The fun doesn't end on functions that take just one parameter, because '~@~' -- lets you build up things like: -- -- > (f ~@~ funOnY) ~@~ funOnX -- > === \g x y -> f (g (funOnX x) (funOnY y)) -- -- As you can se above @g@ is a function that takes two parameters. Now we can -- define @(f '~@~' funOnY)@ separately, then when ever we need we can extend -- it to higher arity function by appending @('~@~' funOnX)@. Special case when -- @funOnY = funOnX@ is very interesting, in example function -- 'Data.Function.on' can be defined using 'between' as: -- -- > on :: (b -> b -> c) -> (a -> b) -> a -> a -> c -- > on f g = (id ~@~ g ~@~ g) f -- > -- or: ((. g) ~@~ g) f -- -- We can also define function @on3@ that takes function with arity three as -- its first argument: -- -- > on3 :: (b -> b -> b -> d) -> (a -> b) -> a -> a -> a -> d -- > on3 f g = (id ~@~ g ~@~ g ~@~ g) f -- > -- or: ((. g) ~@~ g ~@~ g) f -- -- If we once again consider generalizing above examples by using three -- different functions @g1 =\/= g2 =\/= g3@ instead of just one @g@ then we -- get: -- -- > on' :: (b -> b1 -> c) -- > -> (a2 -> b2) -- > -> (a1 -> b1) -- > -> a1 -> a2 -> c -- > on' f g1 g2 = (id ~@~ g2 ~@~ g1) f -- > -- > on3' -- > :: (b1 -> b2 -> b3 -> c) -- > -> (a3 -> b3) -- > -> (a2 -> b2) -- > -> (a1 -> b1) -- > -> a1 -> a2 -> a3 -> c -- > on3' f g1 g2 g3 = (id ~@~ g3 ~@~ g2 ~@~ g1) f -- -- Which allows us to interpret '~@~' in terms like \"apply this function to -- the n-th argument before passing it to the function @f@\". We just have to -- count the arguments backwards. In example if want to apply function @g@ to -- third argument, but no other then we can use: -- -- > \g f -> (id ~@~ g ~@~ id ~@~ id) f -- > -- ^ ^ ^ ^- Applied to the first argument. -- > -- | | '- Applied to the second argument. -- > -- | '- Applied to the third argument. -- > -- '- Applied to the result. -- > :: (a3 -> b3) -> (a1 -> a2 -> b3 -> c) -> a1 -> a2 -> a3 -> c -- -- Or we can use '~@@~', which is just flipped version of '~@~' and then it -- would be: -- -- > \g f -> (id ~@@~ id ~@@~ g ~@@~ id) f -- > -- ^ ^ ^ ^- Applied to the result. -- > -- | | '- Applied to the third argument. -- > -- | '- Applied to the second argument. -- > -- '- Applied to the first argument. -- > :: (a3 -> b3) -> (a1 -> a2 -> b3 -> c) -> a1 -> a2 -> a3 -> c -- -- Another interesting situation is when @f@ and @g@ in @(f '~@~' g)@ form an -- isomorphism. Then we can construct a mapping function that takes function -- operating on one type and transform it in to a function that operates on a -- different type. As we shown before it is also possible to map functions with -- higher arity then one. -- -- Simplicity of how 'between' combinator can be used to define set of -- functions by reusing previous definitions makes it also very suitable for -- usage in TemplateHaskell and generic programming. -- $mappingFunctionsForNewtypes -- -- When we use @(f '~@~' g)@ where @f@ and @g@ form an isomorphism of two -- types, and if @f@ is a constructor and @g@ a selector of newtype, then -- @(f '~@~' g)@ is a mapping function that allows us to manipulate value -- wrapped inside a newtype. -- -- > newtype T t a = T {fromT :: a} -- > -- > mapT -- > :: (a -> b) -- > -> T t a -> T t' b -- > mapT = T ~@~ fromT -- -- Note that @mapT@ above is generalized version of 'fmap' of obvious 'Functor' -- instance for newtype @T@. -- -- Interestingly, we can use 'between' to define higher order mapping functions -- by simple chaining: -- -- > mapT2 -- > :: (a -> b -> c) -- > -> T t1 a -> T t2 b -> T t3 c -- > mapT2 = mapT ~@~ fromT -- > -- or: T ~@~ fromT ~@~ fromT -- > -- or: mapT `between2l` fromT -- > -- > mapT3 -- > :: (a -> b -> c -> d) -- > -> T t1 a -> T t2 b -> T t3 c -> T t4 d -- > mapT3 = mapT2 ~@~ fromT -- > -- or: T ~@~ fromT ~@~ fromT ~@~ fromT -- > -- or: mapT `between3l` fromT -- -- Dually to definition of 'mapT' and 'mapT2' we can also define: -- -- > comapT :: (T a -> T b) -> a -> b -- > comapT = fromT ~@~ T -- > -- or: T ~@@~ fromT -- > -- > comapT2 :: (T a -> T b -> T c) -> a -> b -> c -- > comapT2 = fromT ~@~ T ~@~ T -- > -- or: comapT ~@~ T -- > -- or: T ~@@~ T ~@@~ fromT -- > -- or: T ~@@~ comapT -- > -- or: fromT `between2l` T -- -- In code above we can read code like: -- -- @ -- fromT '~@~' T '~@~' T -- @ -- -- or -- -- @ -- T '~@@~' T '~@@~' fromT -- @ -- -- as \"Apply @T@ to first and second argument before passing it to a function -- and apply @fromT@ to its result.\" -- -- Here is another example with a little more complex type wrapped inside a -- newtype: -- -- > newtype T e a = T {fromT :: Either e a} -- > -- > mapT -- > :: (Either e a -> Either e' b) -- > -> T e a -> T e' b -- > mapT = T ~@~ fromT -- > -- > mapT2 -- > :: (Either e1 a -> Either e2 b -> Either e3 c) -- > -> T e1 a -> T e2 b -> T e3 c -- > mapT2 = mapT ~@~ fromT -- -- This last example is typical for monad transformers: -- -- > newtype ErrorT e m a = ErrorT {runErrorT :: m (Either e a)} -- > -- > mapErrorT -- > :: (m (Either e a) -> m' (Either e' b)) -- > -> ErrorT e m a -> ErrorT e' m' b -- > mapErrorT = ErrorT ~@~ runErrorT -- > -- > mapErrorT2 -- > :: (m1 (Either e1 a) -> m2 (Either e2 b) -> m3 (Either e3 c)) -- > -> ErrorT e1 m1 a -> ErrorT e2 m2 b -> ErrorT e3 m3 c -- > mapErrorT2 = mapErrorT ~@~ runErrorT -- $lenses -- -- Library /lens/ is notorious for its huge list of (mostly transitive) -- dependencies. However it is easy to define a lot of things without the need -- to depend on /lens/ directly. This module defines few functions that will -- make it even easier. -- -- Lens for a simple newtype: -- -- > newtype T a = T {fromT :: a} -- > -- > t :: Functor f => (a -> f b) -> T a -> f (T b) -- > t = fmap T ~@~ fromT -- -- To simplify things we can use function '<~@~': -- -- > t :: Functor f => (a -> f b) -> T a -> f (T b) -- > t = T <~@~ fromT -- -- Lets define lenses for generic data type, e.g. something like: -- -- > data D a b = D {_x :: a, _y :: b} -- -- Their types in /lens/ terms would be: -- -- > x :: Lens (D a c) (D b c) a b -- > y :: Lens (D c a) (D c b) a b -- -- Here is how implementation can look like: -- -- > x :: Functor f => (a -> f b) -> D a c -> f (D b c) -- > x = _x ~@@^> \s b -> s{_x = b} -- -- Alternative definitions: -- -- > x = (\s b -> s{_x = b}) <^@~ _x -- > x f s = (_x ~@@~> \b -> s{_x = b}) f s -- > x f s = ((\b -> s{_x = b}) <~@~ _x) f s -- > x f s = (const _x ^@@^> \s' b -> s'{_x = b}) f s s -- > x f s = ((\s' b -> s'{_x = b}) <^@^ const _x) f s s -- -- And now for @y@ we do mostly the same: -- -- > y :: Functor f => (a -> f b) -> D c a -> f (D c b) -- > y = _y ~@@^> \s b -> s{_y = b} -- -- Above example shows us that we are able to define function equivalent to -- @lens@ from /lens/ package as follows: -- -- > lens -- > :: (s -> a) -- > -- ^ Selector function. -- > -> (s -> b -> t) -- > -- ^ Setter function. -- > -> (forall f. Functor f => (a -> f b) -> s -> f t) -- > -- ^ In /lens/ terms this is @Lens s t a b@ -- > lens = (~@@^>) -- -- Alternative definitions: -- -- > lens get set f s = (const get ^@@^> set) f s s -- > lens get set f s = (set <^@^ const get) f s s -- > lens get set f s = (get ~@~> set s) f s -- > lens get set f s = (set s <~@~ get) f s -- -- Some other functions from -- <http://hackage.haskell.org/package/lens lens package> can be defined using -- '~@~': -- -- @ -- set :: ((a -> Identity b) -> s -> Identity t) -> b -> s -> t -- set = (runIdentity .) '~@~' ('Data.Function.const' . Identity) -- @ -- -- @ -- over :: ((a -> Identity b) -> s -> Identity t) -> (a -> b) -> s -> t -- over = (runIdentity .) '~@~' (Identity .) -- @ -- -- Data type @Identity@ is defined in -- <http://hackage.haskell.org/package/transformers transformers package>.