Suppose, in Haskell, I have a bunch of functions that all depend on the same parameter type:
f :: Par -> a -> b
g :: Par -> b -> c
As I'm writing more of these functions that still depend on this parameter type, I can do something like
h :: Par -> a -> c
h par = myg . myf
where myf = f par
myg = g par
However I keep having to write these where
lines. The question is: can this be avoided?
[Edit: I tried to provide a minimal example to illustrate the problem but apparently the example is too minimal to illustrate what I want. In the actual problem h is of course not just the composition of f and g. So here is some actual code:
There are functions
apply :: ChamberLattice -> ChLatword -> ChLatWord
reduce :: ChamberLattice -> ChLatWord -> ChLatWord
and I am defining a function
chaseTurn :: ChamberLattice -> Turn -> Parity -> ChLatWord -> ChLatWord
chaseTurn cl Straight _ xs = xs
chaseTurn cl t parity xs = if ((turn parity xs) == t)
then case myApply xs of
(y1:y2:ys) -> (y1:y2:(myChaseTurn t parity ys))
ys -> ys
else myReduce xs
where myApply = apply cl
myChaseTurn = chaseTurn cl
myReduce = reduce cl
]
(This question is essentially the same as Grouping functions in Haskell but there I used some unfortunate words that distracted people.)
This can be done using implicit parameters (which are not pure Haskell but a ghc language extension, see https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#implicit-parameters).
The code above just becomes
In Haskell, all functions take one input argument. Sometimes, though, the return value of applying a function is a new function. As a first step, then, you can make that more explicit by putting brackets around the return value of your functions
f
andg
:Functions are types as well, so we could arbitrarily decide to alias
a -> b
toφ
(phi instead of f) andb -> c
toγ
(gamma instead of g). (Yes, when you run out of letters, you reach for the Greek alphabet!)This means that you can view your functions as having the types
These are both automatically instances of the so-called reader monad, which is also an (applicative) functor. Particularly,
(->) Par
, or, if it helps,Par ->
, is anApplicative
instance. This means that you can usepure
and<*>
with it.As a first attempt, you can write something like
in order to simply understand how that composition works. That expression has the type
Par -> (φ, γ)
, so to speak. That lambda expression simply takesx
from thef
'container', andy
from theg
'container', and combines them in a tuple. The first element of the tuple has the typeφ
, and the second element has the typeγ
.Plugging in the definitions of
φ
andγ
, you get the typePar -> (a -> b, b -> c)
.Instead of a return value as a tuple of functions, you want to compose these functions. You can use the function composition operator
.
for that:Notice that the functions compose from right to left, so
x
(a -> b
) comes first, followed byy
(b -> c
).You can, however, flip
f
andg
around:That explicit lambda expression can then be eta-reduced to:
Finally, instead of writing
pure (.) <*>
you can use the infix<$>
operator:This function has the type
Par -> a -> c
.You've discovered the use case for the
Reader
monad, if you can adjust your signatures slightly. If you haveyou can redefine them as
Then you can define
h
using the normal Kleisli composition operator.(Even without changing the signatures, I think you can write
h = flip (flip f >=> flip g)
.)You're doing
h par = f par . g par
a lot, and thepar
stuff starts to clutter.You can't do
h = f . g
, since thepar
argument must be passed along, too.So you come up with a high-powered composition operator that will do this for you:
Now you can do
h = f § g
. This operator was probably invented before.Incidentally, partially applied functions are instances of Monad. This means you can do:
Or just:
(Here,
fpar
isf
to which an implicitpar
has been applied. The monad instance makespar
implicit.)If we were to parameterize this do-block:
And eta-reduce the parameters:
And look on Hoogle for something that looks like this do-block, you'd find
liftM2
:At which point we don't really need to give it a special name, since
liftM2 (.)
is already pretty short.