You are right in thinking that it is currying that allows this behavior. If we look at the definition of Ize
, it's just a type synonym for String -> Integer -> String
. If we plug this in to the type signature of composePref
, we'd get
composePref :: (String -> Integer -> String) -> (String -> Integer -> String) -> (String -> Integer -> String)
(I hope you see now why a type alias was used, it greatly shortens the signature). Since ->
in type signatures is right associative, it means that something like
a -> b -> c -> d
Is equivalent to
a -> (b -> (c -> d))
So we can further simplify the signature to be (with some extra type aliases because I don't want to type them all)
type I = Integer
type S = String
composePref :: (S -> I -> S) -> (S -> I -> S) -> S -> I -> S
composePref f g x = \c -> ...
Then f :: (S -> I -> S)
, g :: (S -> I -> S)
, and x :: S
. I included the beginning of that labmda so that I could say that c :: I
. You could actually write this function as:
composePref :: Ize -> Ize -> Ize
composePref f g x c = let y = (g x) c in (f y) c
Which is also equivalent to
composePref f g x c = let y = g x c in f y c
-- (map show) [1, 2, 3] === map show [1, 2, 3]
Or
composePref f g x c = f (g x c) c
Or even
composePref f g = \x c -> f (g x c) c
These are all equivalent definitions of composePref
. I think the last might make it most clear that it's a function that takes two functions and returns a new function of the same type.
To try to make it even more clear, I'll write some illegal syntax with type annotations where you aren't really supposed to use them:
composePref (f :: Ize) (g :: Ize) = h
where
h :: Ize
h (x :: String) (c :: Integer) =
let (y :: String) = (g x) c
in (f y) c