Haskell is a purely functional language, meaning that functions are expected to be “pure” and behave in predictable ways without side effects. This concept of purity is central to Haskell and has profound implications for how programs are written, understood, and maintained. Haskell also has a function called pure
, which is part of the Applicative
type class. Though related to the concept of purity, pure
has a specific purpose in Haskell’s type system. This article will explain both the notion of purity in Haskell and the role of the pure
function.
Purity in Haskell
Purity in programming refers to functions that behave predictably and depend solely on their inputs to produce outputs, without affecting or depending on any outside state. In Haskell, this concept is built into the language, making it possible to reason about code more easily and ensuring consistency.
What Makes a Function Pure?
A pure function has two main properties:
- Deterministic Behavior: A pure function always produces the same output when given the same input. There’s no randomness, and it doesn’t depend on any external state.
- No Side Effects: Pure functions don’t alter any state outside of themselves. They don’t perform I/O operations, change variables, modify data, or interact with the outside world. They simply compute and return a result.
For example, the following function is pure:
square :: Int -> Int
square x = x * x
No matter when or where you call square 3
, it will always return 9
and will not alter any external state.
Why is Purity Important?
Purity in Haskell brings several advantages:
- Referential Transparency: Because pure functions always produce the same results for the same inputs, you can replace a function call with its result without affecting the program’s behavior. This property makes code easier to understand, refactor, and test.
How Does Haskell Handle Side Effects?
While purity is fundamental in Haskell, practical programming often requires side effects, like reading user input, writing files, or generating random numbers. Haskell manages side effects using monads (such as IO
, Maybe
, and Either
), which encapsulate effects and allow the language to maintain purity while managing real-world interactions.
With monads, Haskell separates pure computations from effectful ones. For example, functions that read from or write to the outside world use the IO
monad, isolating side effects from pure computations. This design enables Haskell to keep core computations pure while still interacting with the real world.
Understanding the pure
Function
Now that we understand purity, let’s look at the pure
function. Although its name might suggest a connection to purity, pure
actually comes from the Applicative
type class and has a distinct purpose related to Haskell’s type system.
The pure
Function in Applicative
The pure
function is part of the Applicative
type class. Its purpose is to take a value and lift it into an applicative context. It has the following type signature:
pure :: a -> f a
Here, a
is a regular value, and f
represents an applicative functor, like Maybe
, List
, or IO
. The pure
function wraps a
in an applicative, creating a value of type f a
.
Examples of pure
with Different Applicatives
With Maybe
:
pure 5 :: Maybe Int
-- Result: Just 5
In this case, pure
takes the value 5
and puts it into a Maybe
context, resulting in Just 5
.
With List
:
pure 5 :: [Int]
-- Result: [5]
For lists, pure
creates a singleton list containing the value.
With IO
:
pure 5 :: IO Int
-- Result: an IO action that yields 5
Here, pure
creates an IO
action that produces the value 5
when executed.
Why is pure
Useful?
The pure
function is useful for bringing regular values into contexts where they can be combined with other values in the same context. For example, in the context of Maybe
, pure
allows you to wrap a plain value as a “successful” computation (Just value
), while in IO
, pure
lets you lift values into the world of side effects.
The pure
function is particularly helpful when working with other applicative operations, such as <*>
(apply), because it enables you to bring normal values into an applicative context to work with functions already wrapped in that context.
Example: Using pure
with <*>
Suppose you have a function that adds two integers and you want to apply it to values inside the Maybe
context:
add :: Int -> Int -> Int
add x y = x + y
If add
is not yet in the Maybe
context, you can use pure
to lift it into that context:
pure add <*> Just 3 <*> Just 5
-- Result: Just 8
Here’s what’s happening step-by-step:
pure add
lifts theadd
function into theMaybe
context:Maybe (Int -> Int -> Int)
.<*>
appliespure add
(inMaybe
) toJust 3
, resulting inJust (Int -> Int)
.<*>
applies that result toJust 5
, producingJust 8
.
Summary: Purity and pure
in Haskell
While they share the same word root, purity and pure
in Haskell refer to different but related ideas:
- Purity: In Haskell, purity is the property that functions depend only on their inputs and have no side effects. Pure functions are deterministic and referentially transparent, which is central to Haskell’s design philosophy.
- The
pure
Function: Thepure
function is part of theApplicative
type class and is used to lift a regular value into an applicative context. This allows functions and values within a context, likeMaybe
,List
, orIO
, to be combined and manipulated in a uniform way.
Together, these concepts allow Haskell to support powerful abstractions, enabling expressive and type-safe code that separates pure computation from side effects while allowing seamless integration when needed. Understanding both purity and the pure
function will help you write clear, reliable Haskell programs that harness the full power of functional programming.
Leave a Reply