Experimenting with More Functions in Haskell

0
3787

Gear-Conceptual-visual

We continue our exploration of the open source, advanced and purely functional programming language, Haskell. In the third article in the series, we will focus on more Haskell functions, conditional constructs and their usage.

A function in Haskell has the function name followed by arguments. An infix operator function has operands on either side of it. A simple infix add operation is shown below:

*Main> 3 + 5
8

If you wish to convert an infix function to a prefix function, it must be enclosed within parentheses:

*Main> (+) 3 5
8

Similarly, if you wish to convert a prefix function into an infix function, you must enclose the function name within backquotes(`). The elem function takes an element and a list, and returns true if the element is a member of the list:

*Main> 3 `elem` [1, 2, 3]
True
*Main> 4 `elem` [1, 2, 3]
False

Functions can also be partially applied in Haskell. A function that subtracts ten from a given number can be defined as:

diffTen :: Integer -> Integer
diffTen = (10 -)

Loading the file in GHCi and passing three as an argument yields:

*Main> diffTen 3
7

Haskell exhibits polymorphism. A type variable in a function is said to be polymorphic if it can take any type. Consider the last function that returns the last element in an array. Its type signature is:

*Main> :t last
last :: [a] -> a

The ‘a’ in the above snippet refers to a type variable and can represent any type. Thus, the last function can operate on a list of integers or characters (string):

*Main> last [1, 2, 3, 4, 5]
5
*Main> last "Hello, World"
'd'

You can use a where clause for local definitions inside a function, as shown in the following example, to compute the area of a circle:

areaOfCircle :: Float -> Float
areaOfCircle radius = pi * radius * radius
where pi = 3.1415

Loading it in GHCi and computing the area for radius 1 gives:

*Main> areaOfCircle 1
3.1415

You can also use the let expression with the in statement to compute the area of a circle:

areaOfCircle :: Float -> Float
areaOfCircle radius = let pi = 3.1415 in pi * radius * radius

Executing the above with input radius 1 gives:

*Main> areaOfCircle 1
3.1415

Indentation is very important in Haskell as it helps in code readability — the compiler will emit errors otherwise. You must make use of white spaces instead of tab when aligning code. If the let and in constructs in a function span multiple lines, they must be aligned vertically as shown below:

compute :: Integer -> Integer -> Integer
compute x y =
let a = x + 1
b = y + 2
in
a * b

Loading the example with GHCi, you get the following output:

*Main> compute 1 2
8

Similarly, the if and else constructs must be neatly aligned. The else statement is mandatory in Haskell. For example:

sign :: Integer -> String
sign x =
if x > 0 
then "Positive"
else
if x < 0
then "Negative"
else "Zero"

Running the example with GHCi, you get:

*Main> sign 0
"Zero"
*Main> sign 1
"Positive"
*Main> sign (-1)
"Negative"

The case construct can be used for pattern matching against possible expression values. It needs to be combined with the of keyword. The different values need to be aligned and the resulting action must be specified after the ‘->’ symbol for each case. For example:

sign :: Integer -> String
sign x =
case compare x 0 of
LT -> "Negative"
GT -> "Positive"
EQ -> "Zero"

The compare function compares two arguments and returns LT if the first argument is lesser than the second, GT if the first argument is greater than the second, and EQ if both are equal. Executing the above example, you get:

*Main> sign 2
"Positive"
*Main> sign 0
"Zero"
*Main> sign (-2)
"Negative"

The sign function can also be expressed using guards (‘|’) for readability. The action for a matching case must be specified after the ‘=’ sign. You can use a default guard with the otherwise keyword:

sign :: Integer -> String
sign x
| x > 0 = "Positive"
| x < 0 = "Negative"
| otherwise = "Zero"

The guards have to be neatly aligned:

*Main> sign 0
"Zero"
*Main> sign 3
"Positive"
*Main> sign (-3)
"Negative"

There are three very important higher order functions in Haskell — map, filter and fold.
The map function takes a function and a list, and applies the function to each and every element of the list. Its type signature is:

*Main> :t map
map :: (a -> b) -> [a] -> [b]

The first function argument accepts an element of type ‘a’ and returns an element of type ‘b’. An example of adding two to every element in a list can be implemented using map:

*Main> map (+ 2) [1, 2, 3, 4, 5]
[3,4,5,6,7]

The filter function accepts a predicate function for evaluation, and a list, and returns the list with those elements that satisfy the predicate. For example:

*Main> filter (> 0) [-2, -1, 0, 1, 2]
[1,2]

Its type signature is:

filter :: (a -> Bool) -> [a] -> [a]

The predicate function for filter takes as its first argument an element of type ‘a’ and returns True or False.
The fold function performs a cumulative operation on a list. It takes as arguments a function, an accumulator (starting with an initial value) and a list. It cumulatively aggregates the computation of the function on the accumulator value as well as each member of the list. There are two types of folds — left and right fold.

*Main> foldl (+) 0 [1, 2, 3, 4, 5]
15
*Main> foldr (+) 0 [1, 2, 3, 4, 5]
15

Their type signatures are, respectively:

*Main> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
*Main> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b

The way the fold is evaluated among the two types is different and is demonstrated below:

*Main> foldl (+) 0 [1, 2, 3]
6
*Main> foldl (+) 1 [2, 3]
6
*Main> foldl (+) 3 [3]
6

It can be represented as ‘f (f (f a b1) b2) b3’ where ‘f’ is the function, ‘a’ is the accumulator value, and ‘b1’, ‘b2’ and ‘b3’ are the elements of the list. The parenthesis is accumulated on the left for a left fold. The computation looks like this:

*Main> (+) ((+) ((+) 0 1) 2) 3
6
*Main> (+) 0 1
1
*Main> (+) ((+) 0 1) 2
3
*Main> (+) ((+) ((+) 0 1) 2) 3
6

With the recursion, the expression is constructed and evaluated only when it is finally formed. It can thus cause stack overflow or never complete when working with infinite lists. The foldr evaluation looks like this:

*Main> foldr (+) 0 [1, 2, 3]
6
*Main> foldr (+) 0 [1, 2] + 3
6
*Main> foldr (+) 0 [1] + 2 + 3
6

It can be represented as ‘f b1’ (f b2 (f b3 a)) where f is the function, ‘a’ is the accumulator value, and ‘b1’, ‘b2’ and ‘b3’ are the elements of the list. The computation looks like this:

*Main> (+) 1 ((+) 2 ((+) 3 0)) 
6
*Main> (+) 3 0
3
*Main> (+) 2 ((+) 3 0)
5
*Main> (+) 1 ((+) 2 ((+) 3 0))
6

There are some statements like condition checking where ‘f b1’ can be computed even without requiring the subsequent arguments, and hence the foldr function can work with infinite lists. There is also a strict version of foldl (foldl’) that forces the computation before proceeding with the recursion.
If you want a reference to a matched pattern, you can use the as pattern syntax. The tail function accepts an input list and returns everything except the head of the list. You can write a tailString function that accepts a string as input and returns the string with the first character removed:

tailString :: String -> String
tailString "" = ""
tailString input@(x:xs) = "Tail of " ++ input ++ " is " ++ xs

The entire matched pattern is represented by input in the above code snippet.
Functions can be chained to create other functions. This is called ‘composing’ functions. The mathematical definition is as under:

(f o g)(x) = f(g(x))

This dot (.) operator has the highest precedence and is left-associative. If you want to force an evaluation, you can use the function application operator ($) that has the second highest precedence and is right-associative. For example:

*Main> (reverse ((++) "yrruC " (unwords ["skoorB", "lleksaH"])))
"Haskell Brooks Curry"

You can rewrite the above using the function application operator that is right-associative:

Prelude> reverse $ (++) "yrruC " $ unwords ["skoorB", "lleksaH"]
"Haskell Brooks Curry"

You can also use the dot notation to make it even more readable, but the final argument needs to be evaluated first; hence, you need to use the function application operator for it:

*Main> reverse . (++) "yrruC " . unwords $ ["skoorB", "lleksaH"]
"Haskell Brooks Curry"

LEAVE A REPLY

Please enter your comment!
Please enter your name here