Haskell Tutorial: Get started with functional programming
Ryan Thelin
Posted on February 26, 2021
Haskell is a classic functional programming language making a resurgence in the 2020s. As the demand for data scientists grows, companies are looking for tools that can scale with big data volumes and maintain efficiency.
Haskell is perfect for the job with years of optimizations and features built especially for this kind of business data analysis.
Today, we'll help you overcome functional programming's learning curve with a hands-on introduction to Haskell.
Here’s what we’ll cover today:
- What is functional programming?
- What is the Haskell programming language?
- Basics of Haskell Syntax
- Advanced Haskell Concepts
- What to learn next
Transition to functional programming fast
Skip the functional programming learning curve with hands-on Haskell practice.
Learn Functional Programming in Haskell
What is functional programming?
Functional programming is a declarative programming paradigm used to create programs with a sequence of simple functions rather than statements.
While OOP programs excel at representing physical objects with unique traits, functional programs are purely mathematical and are used for complex mathematical computations or non-physical problems such as AI design or advanced equation models.
All functions in the functional paradigm must be:
- Pure: They do not create side effects or alter the input data
- Independent from program state: The value of the same input is always the same, regardless of other variable values.
Each function completes a single operation and can be composed in sequence to complete complex operations. For example, we might have one function that doubles an input number, doubleInput
, another that divides the number by pi, divPi
.
Either of these functions can be used individually or they can be strung together such that the output of doubleInput
is the input of divPi
. This quality makes pieces of a functional program highly modular because functions can be reused across the program and can be called, passed as parameters, or returned.
What is the Haskell programming language?
Haskell is a compiled, statically typed, functional programming language. It was created in the early 1990s as one of the first open-source purely functional programming languages and is named after the American logician Haskell Brooks Curry. Haskell joins Lisp as an older but useful functional language based in mathematics.
The Haskell language is built from the ground up for functional programming, with mandatory purity enforcement and immutable data throughout. It's mainly known for its lazy computation and memory safety that avoids memory management problems common in languages like C or C++.
It also has a more robust selection of data types than other statically typed languages like Java, featuring typing features like parametric polymorphism, class-based (ad-hoc) polymorphism, type families, and more.
Overall, Haskell compounds the performance and scalability advantages of functional programming with years of optimizations and unique tools.
Now, Haskell is primarily used in data analysis for data-rich business fields like finance, biotech, or eCommerce. These industries' growing demand for scalability and safety make Haskellers a highly sought-after group.
Here's an example of how an imperative solution in Python would look as a declarative functional solution in Haskell:
def compound_interest(years):
current_money = 1000
for year in range(years):
current_money = current_money * 1.05
print('After {years} years, you have {money:.2f} dollars.'.format(years=years, money=current_money))
return current_money
compound_interest(10)
compoundInterest :: Int -> Double
compoundInterest 0 = 1000
compoundInterest n = 1.05 * (compoundInterest (n - 1))
main = printf "After 10 years, you have %.2f dollars." (compoundInterest 10)
Compared to the imperative program, the Haskell program has:
- Type and static type annotations.
- No statements. The function is defined case by case with expressions.
- No loop. We use recursive calls to multiply the interest rate each time.
- No mutable variable. We use a recursive expression to obtain the value after n years from the value after (n - 1) years.
- No side effects inside this function. Printing the result to the screen happens outside of the function that computes the result.
Salient features of Haskell
Memory Safe
Includes automatic memory management to avoid memory leaks and overflows. Haskell's memory management is similar to that of Golang, Rust, or Python
Compiled
Uses the GHC Haskell compiler to compile directly to machine source code ahead of time. GHC is highly optimized and generates efficient executables to increase performance. It also has an interactive environment called GHCi that allows for expressions to be evaluated interactively. This feature is the key to Haskell's popularity for high input data analytics.
Statically Typed
Has a static type system similar to Java that validates Haskell code within the environment. This lets you catch bugs during development earlier on. Haskell's great selection of types means you always have the perfect type for a given variable.
Enforced Functional Best Practices
Enforces functional programming rules such as pure functions and immutable variables with error messages. This feature minimizes complexity in your program and ensures you're making the best use of its functional capabilities.
Lazy Evaluation
Defers computation until the results are needed. Haskell is well known for its optimized lazy evaluation capabilities that make refactoring and function composition easy.
Concurrency
Haskell makes concurrency easy with green threads (virtual threads) and async
and stm
libraries that give you all the tools you need to create concurrent programs without a hassle. Enforced pure functions add to the simplicity and sidestep many of the usual problems of concurrent programming.
Libraries
Haskell has been open source for several decades at this point, meaning there are thousands of libraries available for every possible application. You can be certain that almost all the problems you encounter will have a library already made to solve them. Some popular additions are Stack, which builds and handles dependencies, and Cabal, which adds packaging and distribution functionality.
Basics of Haskell Syntax
Now that you know why Haskell is still used today, let's explore some basic syntax. The two most central concepts of Haskell are types and functions.
- Types are collections of values that behave similarly, e.g., numbers or strings.
- Functions can be used to map values of one type to another.
Numeric Types
Numeric types hold numerical values of different ranges and digit numbers, such as 15
or 1.17
. Haskell has 3 common numeric types:
-
Int
for 64 bit (>20 digit)integers -
Integer
list ofInt
types that can represent any number (similar toBigInt
in other languages) -
Double
for 64-bit decimal numbers Each numeric type works with standard operators like+
,-
, and*
. OnlyDouble
supports division operations and allInteger
divisions will return as aDouble
. For example:
Prelude> 3 / 2
1.5
Here are some examples of more operations with numeric types.
Prelude> 1 + 2
3
Prelude> 5 * (7 - 1)
30
Haskell uses type inference to assign the most logical data type for a given operation. As a result, we don't have to declare types if it is “obvious” such as Int
vs. Double
values.
To explicitly declare the data types, you can add designations after each value like so:
Prelude> (1 :: Int) + (2 :: Int)
3
Haskell also includes predefined functions for common numerical operations like exponents, integer division, and type conversion.
- Power (
^
): Raises the first number to the power of the second number. This executes several hidden multiplication operations and returns the final result. - Integer Division (
div
): Used to complete division on integers without changing to a double. All decimals are truncated. There is also the modulus operator (mod
) that lets you find the remainder.
Prelude> div 7 3
2
Prelude> mod 7 3
1
- Type conversion: Haskell doesn't support cross-type operations, meaning we often have to convert values. Prelude includes type conversions from different common types, such as
fromIntegral
orfromDouble
.
Prelude> 5.2 + fromIntegral (div 7 3)
7.2
Strings
String types represent a sequence of characters that can form a word or short phrase. They're written in double quotes to distinguish them from other data types, like "string string".
Some essential string functions are:
-
Concatenation: Join two strings using the
++
operator
Prelude Data.Char> "hello, " ++ "world" "hello, world"
-
Reverse: Reverses the order of characters in a String such that the first character becomes the last
Prelude Data.Char> reverse "hello" "olleh" Prelude Data.Char> reverse "radar" "radar"
Tuples
Tuble types is a data type that contains two linked values of preset value. For example, (5, True)
is a tuple containing the integer 5
and the boolean True
. It has the tuple type (Int, Bool)
, representing values that contain first an Int
value and second a Bool
value.
twoNumbers :: (Double, Double)
twoNumbers = (3.14, 2.59)
address :: (String, Int, String, Int)
address = ("New York", 10005, "Wall St.", 1)
main = do
print twoNumbers
print address
Tuple construction is essentially a function that links two values such that they're treated as one.
Custom functions
To create your own functions, using the following definition:
function_name :: argument_type -> return_type
The function name is what you use to call the function, the argument type defines the allowed data type for input parameters, and return type defines the data type the return value will appear in.
After the definition, you enter an equation that defines the behavior of the function:
function_name pattern = expression
The function name echoes the name of the greater function, pattern acts as a placeholder that will be replaced by the input parameter, and expression outlines how that pattern is used.
Here's an example of both definition and equation for a function that will print a passed String twice.
sayTwice :: String -> String
sayTwice s = s ++ s
main = print (sayTwice "hello")
Lists
Lists are a recursively defined sequence of elements. Like Linked Lists, each element points to the next element until the final element, which points to a special nill
value to mark the end of the list.
All elements in a list must be the same data type defined using square brackets like [Int]
or [String]
. You then populate the list with a comma-separated series of values of that type. Once populated, all values are immutable in their current order.
ints :: [Int]
ints = [1, 2, 3]
Lists are useful to store data that you'll later need to loop through because they're easily usable with recursion.
Custom Data Types and Type Classes
Haskell also allows you to create your own data types similar to how we create functions. Each data type has a name and a set of expectations for what values are acceptable for that type.
To better understand this, take a look at the standard library's Bool
type definition:
data Bool = False | True
Custom data types are marked with the data
keyword and are named Bool
by the following item. The =
marks the boundary between name and accepted values. Then False | True
defines that any value of type Bool
must be either true or false.
Similarly, we can define a custom Geometry
data type that accepts 3 forms of shapes, each with different input requirements.
data Geometry = Rectangle Double Double | Square Double | Circle Double
Our Geometry
data type allows for the creation of three different shapes: rectangles, squares, and circles.
These shapes are data constructors that define the acceptable values for an element of type Geometry
. A Rectangle
is described by two doubles (its width and height), a Square
is described by one double (the length of one side), and a Circle is described by a single double (its radius).
When creating a Geometry value, you must declare which constructor, Rectangle
, Square
and Circle
, you wish to use for your input. For example:
*Geometry> s1 = Rectangle 3 5 :: Geometry
*Geometry> s2 = Square 4 :: Geometry
*Geometry> s3 = Circle 7 :: Geometry
Type Classes
A type class is a collection of types that share a common property. For example, the type class Show
is the class of all types that can be transformed into a string using the show
function (note the difference in capitalization). Its syntax is:
class Show a where
show :: a -> String
All type class declarations start with the class
keyword, a name (Show
) and a type variable (a
). The where
keyword sets a conditional that calls for all types where the following statement equates as True
. In this case, Show
looks for all types with a show
function that takes a variable and returns a String
.
In other words, every type a
that belongs to the Show
type class must support the show
function. Type classes behave similarly to interfaces of object-oriented programming languages as they define a blueprint for a group of data.
Keep learning Haskell
Transition to Haskell and start working in analytics in half the time. Educative's hands-on text courses let you practice as you learn, without lengthy tutorial videos. You'll even get a certificate of your expertise to share with your employer.
Learn Functional Programming in Haskell
Advanced Haskell concepts
Higher-order Functions
As with other functional programming languages, Haskell treats functions as first-class citizens that can be passed or returned from other functions. Functions that act on or return new functions are called higher-order functions.
You can use higher-order functions to combine your modular functions to complete complex operations. This is an essential part of function composition, where the output of one function serves as the input for the next function.
The function applyTwice
takes a function of integers as its first argument and applies it twice to its second argument.
applyTwice :: (Int -> Int) -> Int -> Int
applyTwice f x = f (f x)
The parentheses clarify that the first Int
set should be read together to mean an Int
function rather than two independent Int
values.
Now we'll create some sample functions double
and next
to pass to our higher-order function applyTwice
.
applyTwice :: (Int -> Int) -> Int -> Int
applyTwice f x = f (f x)
double :: Int -> Int
double x = 2 * x
next :: Int -> Int
next x = x + 1
main = do
print (applyTwice double 2) -- quadruples
print (applyTwice next 1) --adds 2
Lambda expression
Our implementation of applyTwice
above is effective if we want to use double
and next
more than once. But what if this is the only time we'll need this behavior? We don't want to create an entire function for one use.
Instead, we can use Haskell's lambda expression to create an anonymous function. These are essentially one-use functions with expressions defined where they're used but without a name to save it. Lambda expressions otherwise work as functions with input parameters and return values.
For example, we can convert our next
function into a lambda expression:
\x -> x + 1
Lambda expressions always begin with a backslash (\
) and then list a placeholder for whatever is input to the function, x
. Then there is an arrow function to mark the beginning of the expression. The expression uses the input parameter wherever x
is called.
Here, our lambda expression essentially says "add 1 to whatever input I'm passed, then return the new value".
You can also use lambda expressions as input for higher-order functions. Here is how our applyTwice
function works with lambda expressions instead of functions:
applyTwice :: (Int -> Int) -> Int -> Int
applyTwice f = f . f
main = do
print (applyTwice (\x -> x * 2) 8)
print (applyTwice (\x -> x + 1) 7)
Lambda expressions are often used to provide higher-order functions with simple behaviors that you do not want to save to a function or will only need once. You can also use them to outline general logical patterns of your program by supplying them to abstract higher-order functions.
Recursion
Functional languages like Haskell do not have loops or conditional statements like imperative languages. They instead use recursion to create repeated behaviors. This is because recursive structures are declarative, like functional programming, and therefore are a better fit.
Reminder: recursive functions are functions that call themselves repeatedly until a designated program state is reached.
Here's an example of a recursive function in Haskell:
compoundInterest :: Int -> Double
compoundInterest 0 = 1000
compoundInterest n = 1.05 * compoundInterest (n - 1)
main = print (compoundInterest 3)
The first equation covers the base case that executes if the input value is 0
and yields the result 1000
immediately. The second equation is the recursive case, which uses the result of the computation for input value n - 1
to compute the result for input value n
.
Take a look at how this recursive function evaluates over each recursive step:
The switch from loops to recursive structures is one of the most difficult changes to make when adopting Haskell. Complex recursive structures cause a deep nesting effect that can be confusing to understand or debug.
However, Haskell minimizes the severity of this problem by requiring pure functions and using lazy evaluation to avoid issues with the execution order.
What to learn next
Congratulations on taking your first steps to learn Haskell! While it can be challenging to move from an imperative/general-purpose language like JavaScript, Perl, or PHP to Haskell, there are decades of learning materials available to help you.
Some concepts you'll want to learn next are:
- Monad expressions
- Currying
- Multiple recursive calls
- I/O integration
To help you pick up all these important Haskell concepts, Educative has created the course, Learn Functional Programming in Haskell. This helps solidify your Haskell fundamentals with hands-on practice. It even includes several mini-projects along the way to make sure you have everything you need to apply your knowledge.
By the end, you’ll have a new paradigm under your belt, and you can start using functional programming in your own projects.
Happy learning!
Continue reading about functional programming
Posted on February 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.