What does (f .) . g mean in Haskell?

Learn what does (f .) . g mean in haskell? with practical examples, diagrams, and best practices. Covers haskell, functional-programming, pointfree development techniques with visual explanations.

Demystifying (f .). g in Haskell: A Deep Dive into Function Composition

Abstract representation of function composition with arrows and circles, illustrating how functions combine.

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:

  1. g: Let g :: a -> b. This is the function on the right side of the outer composition.
  2. (f .): This is a partially applied . operator. It expects a function h :: x -> y and returns f . h. So, (f .) :: (x -> y) -> (x -> z) (assuming f :: y -> z).
  3. (f .) . g: The outer . composes (f .) and g. This means (f .) . g is a function that takes an argument x and applies (f .) to g 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) (if f :: z -> w, then (f .) :: (y -> z) -> (x -> y) -> (x -> w)). This is a function that takes a function h and returns f . h.
  • The second argument to the outer . is g. Its type is a -> 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.

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 (+)

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.