DEV Community

Cover image for Haskell Tutorial: Get started with functional programming
Ryan Thelin for Educative

Posted on • Originally published at educative.io

Haskell Tutorial: Get started with functional programming

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:

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)
Enter fullscreen mode Exit fullscreen mode
compoundInterest :: Int -> Double
compoundInterest 0 = 1000
compoundInterest n = 1.05 * (compoundInterest (n - 1))
main = printf "After 10 years, you have %.2f dollars." (compoundInterest 10)
Enter fullscreen mode Exit fullscreen mode

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 of Int types that can represent any number (similar to BigInt in other languages)
  • Double for 64-bit decimal numbers Each numeric type works with standard operators like +, -, and *. Only Double supports division operations and all Integer divisions will return as a Double. For example:
Prelude> 3 / 2
1.5
Enter fullscreen mode Exit fullscreen mode

Here are some examples of more operations with numeric types.

Prelude> 1 + 2
3
Prelude> 5 * (7 - 1)
30
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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 or fromDouble.
Prelude> 5.2 + fromIntegral (div 7 3)
7.2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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

Top comments (0)