Purity
Given a function, for example a function f
of type Int -> Bool
, the effect of calling the function on an Int
must be nothing other than returning a Bool
.
For example, f
cannot read a file, write to command line, or start a thread, as a side effect of being called.
Of course, it is possible to do all these things, but requires a change to the type:
- Fewer brackets are fine (
add1AndPrint x = print x >> return (x > 1)
), and are here just for clarity.
This applies not just to functions, but all values:
> let boolean = print "hello" >> return True
> :t boolean
boolean :: IO Bool -- (1)!
> boolean
"hello"
True
x
cannot have typeBool
- it has to mark in its type the fact that it involves the operation of printing.
The benefit of purity¶
Haskell's purity lends itself to modular code, and easy refactoring.
graphicalUserInterface = runWith complexFunction
where
complexFunction :: UserInput -> Picture
complexFunction = ...
runWith = ... -- e.g., a handler function
Suppose we want to replace complexFunction
with simpleFunction
, also of type UserInput -> Picture
.
Because Haskell is pure (see /thinkingfunctionall/purity/#caveats) and so complexFunction
is not creating/mutating global variables, or opening or closing files, we can be confident that there will be no unexpected implications of making the change, such as a subtle error when runWith
takes complexFunction
as input.
Equational reasoning¶
Because of purity, you may always replace a function call in Haskell with its resulting value. For instance, if the value of positionOfWhiteKing chessboard
is "a4"
, then this
is equivalent to
Tip
Use this fact to understand complex programs, by substituting complex expressions for their values:
To work out what this does, we consult the definition of take
(shown here with some aesthetic simplifications for clarity):
Following this definition, we replace take 2 [1,2,3]
(or more explicitly, #1hs take 2 (1 : [2,3])
) with the pattern that it matches:
take 2 (Bishop : [Rook, Bishop])
= Bishop : take (2-1) [Rook, Bishop]
= Bishop : take 1 (Rook : [Bishop])
We can continue in this vein, repeatedly consulting the definition of take
:
= Bishop : take 1 (Rook : [Bishop])
= Bishop : (Rook : take (1 - 1) [Bishop])
= Bishop : (Rook : take 0 [Bishop])
= Bishop : (Rook : [])
= [Bishop, Rook]
This technique is always applicable, no mater how complex the program.
Caveats¶
Haskell allows a backdoor, mainly useful for debugging.
This is the ability for functions to throw an "unsafe" error:
undefined
has the type forall a. a
, so it can appear anywhere in a program and assume the correct type (see here for more details on how polymorphism works).
As such, it is useful as a "to do" marker (see type driven development).
Created: January 11, 2023