Haskell — the purely functional Lang!

Daizy Obura
DataDrivenInvestor

--

Haskell, as the title of this piece suggests is a purely functional programming language and as you may or not know, that suggests that it treats all computations as the evaluation of mathematical functions. Rather, instead of telling the computer what to do, you tell it what data is.

The uniqueness of Haskell is attributed to the following features:

Laziness — expressions are evaluated only when needed and therefore we can evaluate to potentially infinite values.

Side-effecting — In Haskell, function results depend only on their inputs and therefore functions have no side-effects but this ability is expressed in the Input-Output (IO) type because at some point, we will anyway need to change state. We will later look at an example to understand the IO type better.

DDI Editor’s Pick — Learning Path: Haskell: Functional Programming and Haskell

Polymorphism — usually referred to as taking multiple forms. A data type can be polymorphic as can a function. data Tree a = Leaf a | Node (Tree a) (Tree a) . A tree is an example of a polymorphic data type because it could be a tree of integers, strings, characters or whatever. The function id :: a -> a is polymorphic as it takes input of any type and returns that value.

Conciseness — Haskell has a high level of abstraction and uses a number of high level concepts. Programs written with Haskell therefore tend to be shorter, hence readable and easily maintainable.

Take the example functions below (pure and IO) which both return a capitalized string.

Pure function:

capitalize :: String -> String
capitalize [] = []
capitalize (x : xs) = toUpper x ++ capitalize xs

Magnification of the pure function:

  1. Type signature

capitalize :: String -> String indicates that the capitalize function takes a string as input and returns a string. Stating the type signature is not mandatory for most cases because Haskell has the ability to perform type inference but it is recommended as a good practice as it is also necessary for documentation.

2. Pattern Matching

capitalize []       
capitalize (x : xs)

String is just syntactic sugar for [Char], that is, a String is a list of characters. The list data type has two constructors: the empty case and the cons case written as data [] a = [] | a : [a] ((x : xs) translates into x as a head and xs as a tail). The list type has two possible values, the empty list and the non-empty list and it is therefore necessary for us to state all possible return values, hence pattern matching. Data types will be expounded more in the next piece so don’t worry too much about that right now.

Pattern matching consists of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns — Miran Lipovača

As you would expect, calling a function such as capitalize on an empty list would return an empty list, because there is no data to manipulate. The same case is also known as the edge condition .

The next case caters for an actual list of characters and in that case we notice the all powerful recursion .

3. Recursion

capitalize (x : xs) = toUpper x ++ capitalize xs

As earlier stated, purely functional languages are about telling the computer what data is instead of how to reach some result; and recursion is very important and necessary to achieve this. Another reason why recursion is important in this case is because the toUpper function takes a single character as input and produces that character capitalized. It’s signature is written as toUpper :: Char -> Char .

Recall that for the capitalize function, the input and output are both lists of characters, therefore toUpper performed on one character is not sufficient and we can not perform toUpper on the whole string because that will produce a type error! Recursion is therefore our ideal solution in this instance. We concatenate the result of toUpper on the head of the list and the result of the recursive call.

*Lib> capitalize “dee” 
“DEE”

The above function call is evaluated as:

capitalize “dee” 
== toUpper 'd' ++ capitalize “ee”
== 'D' ++ capitalize “ee”
== 'D' ++ (toUpper 'e' ++ capitalize "e")
== 'D' ++ ('E' ++ capitalize "e")
== 'D' ++ ('E' ++ toUpper 'e' ++ capitalize [])
== 'D' ++ ('E' ++ 'E' ++ [])
== 'D' ++ "EE"
== "DEE"

The edge condition we mentioned earlier is very important if we want our recursive function to terminate. The program will still compile without it but produce such an unattractive output when the capitalize function is called:

"DEE*** Exception: src/Lib.hs:12:1-47: Non-exhaustive patterns in function capitalize

IO function:

capitalize' :: IO [Char]
capitalize' = do
str <- getLine
return (map toUpper str)

The signature of capitalize’ suggests that it is of type IO [Char] or IO String and therefore it returns an IO String action. We use the do block to bundle up multiple IO actions.

The getLine function is of type IO String and it is used to ‘pick’ input. Binding getLine to str is necessary to unbox the input received, that is unwrap String from IO String.

The map function has a signature (a -> b) -> [a] -> [b] . Haskell functions are said to take in only one argument but from the signature of the map function, we see that it takes at least two arguments: the function (a -> b) and a variable a. Say what now 😕? Contradictions, right? That will be clear in a minute.

Unveiling the trick: Higher order functions (HOF’s) are functions that have their input or output as a function. Curried functions which are a class of HOF’s are those that can take in more than one argument. Our map function is a good example of a curried function. Let us examine how.

We can re-write the signature ofmap as follows: ((a -> b) -> [a]) -> [b] and refer to it as a partially applied function, which is one that takes as many parameters as we left out.

Using partial application (calling functions with too few parameters, if you will) is a neat way to create functions on the fly so we can pass them to another function or to seed them with some data — Miran Lipovača

When we map toUpper over a String, we are applying toUpper over every character in the String. For example map toUpper “dee” returns DEE . But capitalize’ is of type IO [Char]; we have the [Char], how do we attach the ‘IO’?. Simple! the return function is of type a -> IO a which indicates that return takes in some variable of any kind and returns it as an IO action.

Kudos for making it this far! Haskell is interesting and mostly automagic 😉 as you have seen! Learning Haskell might be difficult in the start because it is purely functional but as you know; practice will always make perfect. Don’t despair! There are enough tools available to learn Haskell for example Hackage, the exhaustive documentation and central package. Learn you a Haskell for great good! by Miran Lipovača is also a great book to follow for beginners. You can also follow this part-series on Haskell for newbies and let us learn a few concepts together.

--

--

Believer in our Lord Jesus Christ | Techie | Web Developer | Interior Decorator | Jeweller | Learning, Unlearning, Re-learning