What does (f .) . g mean in Haskell?
Categories:
Demystifying (f .). g in Haskell: A Deep Dive into Function Composition
Explore the meaning and application of the seemingly complex (f .) . g construct in Haskell, understanding its role in point-free style and function composition.
Haskell's powerful function composition operators allow for concise and elegant code, often leading to a 'point-free' or 'tacit' style. While .
is the standard composition operator, you might occasionally encounter more intricate compositions like (f .) . g
. This article will break down this expression, explaining its components, how it evaluates, and its practical uses in functional programming.
Understanding the Building Blocks: .
and (.)
Before tackling (f .) . g
, let's revisit the fundamental function composition operator .
(dot) and its sectioned form (.)
. The .
operator composes two functions, f
and g
, such that (f . g) x
is equivalent to f (g x)
. It takes the output of g
and feeds it as input to f
.
import Data.Char (toUpper)
-- Standard function composition
-- (f . g) x = f (g x)
-- Example 1: Composing 'show' and 'length'
showLength :: [a] -> String
showLength = show . length
-- Example 2: Composing 'toUpper' and 'head'
firstCharToUpper :: String -> Char
firstCharToUpper = toUpper . head
-- Using showLength [1,2,3] results in "3"
-- Using firstCharToUpper "hello" results in 'H'
Basic function composition with the .
operator
The section (.)
is a partially applied version of the .
operator. When you write (f .)
, you're creating a new function that takes a second function g
and returns f . g
. Similarly, (. g)
takes a function f
and returns f . g
. This partial application is key to understanding more complex compositions.
-- Type signature of (.)
-- (.) :: (b -> c) -> (a -> b) -> (a -> c)
-- (f .) is a function that takes g and returns (f . g)
-- (f .) :: (a -> b) -> (a -> c)
-- Example:
addOne :: Int -> Int
addOne x = x + 1
double :: Int -> Int
double x = x * 2
-- (addOne .) is a function that expects another function
-- let composedFn = (addOne .) double -- composedFn is (addOne . double)
-- composedFn 5 == addOne (double 5) == addOne 10 == 11
Understanding the sectioned (f .)
form of the composition operator
Deconstructing (f .) . g
Now, let's break down (f .) . g
. This expression is a composition of two functions: (f .)
and g
. The outer .
operator composes these two. Remember, (f .)
is itself a function that takes another function as an argument. The g
here is not a simple value, but a function that will be passed to (f .)
.
flowchart TD subgraph Outer Composition A["(f .) . g"] end subgraph Left Operand B["(f .)"] B1["f"] B2["."] B1 -- argument --> B2 end subgraph Right Operand C["g"] end A -- composes --> B A -- composes --> C B -- takes function --> C C -- becomes argument to --> B D["Result: A function that takes an argument 'x'"] E["Inner composition: (f . g)"] F["Final application: (f . g) x"] A --> D D --> E E --> F
Step-by-step breakdown of the (f .) . g
composition
Let's trace the types and evaluation:
g
: Letg :: a -> b
. This is the function on the right side of the outer composition.(f .)
: This is a partially applied.
operator. It expects a functionh :: x -> y
and returnsf . h
. So,(f .) :: (x -> y) -> (x -> z)
(assumingf :: y -> z
).(f .) . g
: The outer.
composes(f .)
andg
. This means(f .) . g
is a function that takes an argumentx
and applies(f .)
tog x
. However, this is incorrect. The.
operator expects functions as its arguments, not values.
Let's re-evaluate. The .
operator has the type (b -> c) -> (a -> b) -> (a -> c)
. So, (f .) . g
means:
- The first argument to the outer
.
is(f .)
. Its type is(y -> z) -> (x -> y) -> (x -> z)
(iff :: z -> w
, then(f .) :: (y -> z) -> (x -> y) -> (x -> w)
). This is a function that takes a functionh
and returnsf . h
. - The second argument to the outer
.
isg
. Its type isa -> b
.
Therefore, (f .) . g
is a function that takes an argument x
and applies (f .)
to the result of g x
. This is still not quite right for the standard .
operator. The key is that (f .)
is a function that returns a function (f . h
), and g
is a function that returns a value (g x
).
The expression (f .) . g
is actually a composition where g
is applied first, and its result is then passed to (f .)
. But (f .)
expects a function, not a value. This implies that g
must itself return a function. This is where the confusion often lies.
Let's assume g
is a function that returns a function. For example, g :: a -> (b -> c)
. Then (f .) . g
would mean:
((f .) . g) x = (f .) (g x)
Since (f .)
takes a function h
and returns f . h
, if g x
evaluates to a function h'
, then (f .) (g x)
becomes f . (g x)
. This is the correct interpretation.
(f .) . g
is most commonly seen when g
is a function that produces another function. For example, g
might be a higher-order function that takes an argument and returns a partially applied function.Practical Example: Working with map
and filter
Consider a scenario where you want to apply a function f
to the result of mapping another function h
over a list. If h
itself is generated by a function g
, then (f .) . g
becomes very useful.
import Data.List (intercalate)
-- Let's define some simple functions
showInt :: Int -> String
showInt = show
addPrefix :: String -> String -> String
addPrefix prefix str = prefix ++ str
-- We want to create a function that takes a prefix
-- and returns a function that adds that prefix to a string.
-- This is our 'g' function that returns a function.
makePrefixer :: String -> (String -> String)
makePrefixer prefix = addPrefix prefix
-- Now, let's use (showInt .) . makePrefixer
-- Type of (showInt .) :: (a -> Int) -> (a -> String)
-- Type of makePrefixer :: String -> (String -> String)
-- So, (showInt .) . makePrefixer :: String -> (String -> String)
-- This is incorrect. Let's re-evaluate the types carefully.
-- Correct type analysis:
-- f = showInt :: Int -> String
-- (f .) = (showInt .) :: (a -> Int) -> (a -> String)
-- g = makePrefixer :: String -> (String -> String)
-- The outer composition is (X . Y)
-- X = (showInt .) :: (b -> Int) -> (b -> String)
-- Y = makePrefixer :: String -> (String -> String)
-- For X . Y to work, the output type of Y must match the input type of X.
-- Output of Y: (String -> String)
-- Input of X: (b -> Int)
-- These do not match directly.
-- This means the interpretation of (f .) . g as (f .) (g x) is correct, but g must return a function
-- whose output type matches the input type of f.
-- Let's redefine the example to fit the pattern:
-- Suppose we have a function 'f' that operates on a list of strings:
-- f :: [String] -> String
f = intercalate ", "
-- And we have a function 'g' that takes a list of Ints and returns a function
-- that maps over them, converting them to strings.
-- g :: [Int] -> (Int -> String) -> [String]
-- This is not quite right for the (f .) . g pattern.
-- Let's use a simpler, more direct example for (f .) . g
-- f :: b -> c
-- g :: a -> (x -> b) -- g returns a function
-- Then (f .) . g :: a -> (x -> c)
-- Example:
-- f = length :: [a] -> Int
-- g = replicate :: Int -> a -> [a] -- g takes an Int, returns a function (a -> [a])
-- Let's try to compose length with replicate
-- (length .) :: ([a] -> Int) -> ([a] -> Int)
-- (length .) :: (x -> [a]) -> (x -> Int)
-- replicate :: Int -> a -> [a]
-- So, g = replicate 3 'a' :: [Char]
-- This is not a function that returns a function.
-- Let's consider a scenario where 'g' produces a function that we then want to compose with 'f'.
-- f = negate :: Int -> Int
-- g = (+) :: Int -> Int -> Int
-- We want to create a function that takes an Int 'y', and returns a function
-- that adds 'y' to its input, and then negates the result.
-- This would be: \x -> negate (x + y)
-- Let's try to achieve this with (negate .) . (+)
-- (+) :: Int -> (Int -> Int) -- Partially applied (+ y) is (Int -> Int)
-- So, g = (+) :: Int -> (Int -> Int)
-- f = negate :: Int -> Int
-- (f .) :: (a -> Int) -> (a -> Int)
-- (negate .) :: (a -> Int) -> (a -> Int)
-- Now, compose (negate .) with (+)
-- ((negate .) . (+)) :: Int -> (Int -> Int)
-- Let's test it:
-- let composedFn = ((negate .) . (+))
-- composedFn 5 :: (Int -> Int)
-- let finalFn = composedFn 5
-- finalFn 10 == negate (10 + 5) == negate 15 == -15
-- This works! The type of ((negate .) . (+)) is Int -> (Int -> Int).
-- It takes an Int (the 'y' in our example), and returns a function (the '\x -> negate (x + y)').
-- So, ((negate .) . (+)) y x = negate (x + y)
-- This is equivalent to: \y x -> (negate . (y +)) x
-- Which is: \y x -> negate (y + x)
-- Or, more simply: \y -> negate . (y +)
-- This is a common pattern for creating a function that takes an argument, and then returns
-- a new function that has 'f' composed with the result of applying 'g' to that argument.
-- Example: Create a function that takes a multiplier, and returns a function
-- that adds 10 to its input, then multiplies by the given multiplier.
-- f = (*) :: Int -> Int -> Int
-- g = (+10) :: Int -> Int
-- We want: \multiplier x -> multiplier * (x + 10)
-- This is: \multiplier -> (multiplier *) . (+10)
-- Let's try to use (f .) . g for this.
-- f = (*) :: Int -> Int -> Int
-- (f .) = ((*) multiplier .) :: (a -> Int) -> (a -> Int)
-- This is not quite right. The 'f' in (f .) must be a single function.
-- Let's stick to the negate example, which clearly demonstrates the pattern:
-- f = negate :: Int -> Int
-- g = (+) :: Int -> (Int -> Int)
-- ((negate .) . (+)) :: Int -> (Int -> Int)
-- This function takes an Int (let's call it 'y') and returns a function.
-- The returned function is `negate . (y +)`.
-- So, `((negate .) . (+)) y` is equivalent to `negate . (y +)`.
-- And `(negate . (y +)) x` is `negate (y + x)`.
-- This pattern is useful for creating a 'function factory' where the factory's output function
-- is composed with another function 'f'.
A practical example of (f .) . g
with negate
and (+)
(f .) . g
is equivalent to \x -> f . (g x)
. It creates a function that takes an argument x
, applies g
to it, and then composes f
with the result of g x
(which must be a function itself).Why Use This Point-Free Style?
The (f .) . g
construct, like other point-free expressions, can make code more concise and sometimes more readable by focusing on the transformation of data rather than the explicit arguments. It emphasizes function composition as a primary building block. While it can be initially confusing, mastering such constructs deepens your understanding of Haskell's type system and functional programming paradigms.
-- Point-free version:
pointFreeExample :: Int -> (Int -> Int)
pointFreeExample = ((negate .) . (+))
-- Equivalent pointful version:
pointfulExample :: Int -> (Int -> Int)
pointfulExample y = \x -> negate (x + y)
-- Or, using composition explicitly:
pointfulExample' :: Int -> (Int -> Int)
pointfulExample' y = negate . (y +)
-- Both pointFreeExample 5 10 and pointfulExample 5 10 yield -15
Comparing point-free and pointful versions of the same logic
The choice between point-free and pointful style often comes down to readability and the complexity of the expression. For simple cases, point-free can be elegant. For more complex scenarios, explicit arguments might improve clarity. However, understanding these constructs is crucial for reading and comprehending advanced Haskell code.