Defining Functions
A function in Haskell is strictly a device for turning parameters into return values. It won't perform any output, increment a counter, or clear a list. Under the constraints of immutability, any meaningful function will have parameters—since parameters are the only way for the function's behavior to change. Likewise, any meaningful function will return a value, since there's nothing else that a function can do. Parameters and return values are declared in an arrow sequence in a function's type signature. Consider this signature for a function that computes the complement of a proportion:
complement :: Double -> Double
The function's name appears first, and the parameter and return types appear after the ::
. Multiple parameters just add new stops in the arrow sequence. This function accepts a first, middle, and last name and returns their initials:
initials3 :: String -> String -> String -> String
The return type is always the last item in the arrow sequence, and the parameters are the preceding items.
In many languages, the formal parameters are announced in the function header as a series of types and names. In Haskell, only the names appear in the header. The types only appear in the type signature, which is separate from the function definition and is optional. If we don't enter a type signature, the Haskell compiler will infer it from the function definition.
The body of the function is a single expression:
complement :: Double -> Double
complement x = 1 - x
A function is called with relatively quiet syntax:
match = complement 0.7 -- yields 0.3
Test this code on your own computer. Put the definition in a file named sandbox.hs
. Navigate to the file's directory in a terminal and run these commands in ghci
:
> :load sandbox
> complement 0.7
Here's a similarly short function that encloses a string in square brackets:
embracket :: String -> String
embracket contents = "[" ++ contents ++ "]"
tag = embracket "code" -- yields "[code]"
Add the definition of embracket
to sandbox.hs
and test it in ghci
with these commands:
> :reload
> embracket "code"
The :reload
command re-executes the most recently loaded source file. Use :l
and :r
for short.
There are no parentheses in a function call. In fact, there are only two occasions in which we should use them. One is to elevate an operator's precedence, as is often needed when passing a negated expression to a function:
complement (-0.3) -- yields 1.3
-- complement -0.3 -- fails; parses as (complement) - 0.3
The parentheses on the first line elevate the precedence of the -
operator. Perhaps surprisingly, function calls have higher precedence than -
, so without the parentheses the compiler thinks we are trying to call complement
with no parameters and subtract 0.3 from the result. Writing (-0.3)
forces the negation to be evaluated first.
Having to parenthesize unary negations is an annoying quirk of Haskell. Languages that are decorated with delimiters like parentheses and separators like commas don't have these problems, but they have noisy syntax. It's a tradeoff.
The other occasion to use parentheses is to form a tuple. This swap
function accepts an xy-coordinate pair as a 2-tuple and yields a new tuple in which the coordinates have traded places:
swap :: (Double, Double) -> (Double, Double)
swap xy = (snd xy, fst xy)
rotated = swap (11, 5) -- yields (5, 11)
If we try to use parentheses around lists of actual parameters like we do in other languages, we will form a tuple.
Suppose we want to turn three words or names into a three-letter acronym. We might write this function:
initials3 :: String -> String -> String -> String
initials3 first middle last = [
head first,
head middle,
head last
]
short = initials3 "today" "i" "learned" -- yields "til"
This is a fine definition. If we wrote it in an imperative language, we might have stored the three head characters in local variables to shorten up the code that forms the list. We can do the same in Haskell using a where
clause. The where
clause is a series of variable declarations that appear after the expression but which are evaluated before it:
initials3 :: String -> String -> String -> String
initials3 first middle last = [f, m, l]
where f = head first
m = head middle
l = head last
Alternatively, Haskell provides a let
expression in which the variable declarations appear first and the expression in which they are bound appears second:
initials3 :: String -> String -> String -> String
initials3 first middle last =
let f = head first
m = head middle
l = head last
in [f, m, l]
Shortening code isn't the only reason to use let
or where
. If we reference a value more than once, storing the result in a variable avoids redundant calculations. For example, this function turns a pair of numbers into a pair of proportions by dividing both numbers by their total, which is calculated just once:
proportions :: Fractional a => (a, a) -> (a, a)
proportions scores = (us / total, them / total)
where us = fst scores
them = snd scores
total = us + them
weights = proportions (15, 5) -- yields (0.75, 0.25)