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.
Transition to functional programming fast
Skip the functional programming learning curve with hands-on Haskell practice.
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:
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.
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 science 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:
The fastest way to get a modern Haskell toolchain is GHCup, which installs:
PATH.Stack:
stack new myapp simple && cd myapp && stack run
Cabal:
cabal init -n && cabal run
ghci
Prelude> :load Main.hs
Prelude> :type map
Prelude> :reload
Prelude> :set -Wall
Use GHCi to explore types, test functions quickly, and iterate without a full build.
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.
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.
Idiomatic Haskell code leans heavily on pattern matching, guards, and local bindings.
area :: Geometry -> Double
area (Rectangle w h) = w * h
area (Square s) = s * s
area (Circle r) = pi * r * r
grade :: Int -> Char
grade n
| n >= 90 = 'A'
| n >= 80 = 'B'
| n >= 70 = 'C'
| otherwise = 'D'
bmiTell :: Double -> Double -> String
bmiTell w h
| bmi < 18.5 = "Underweight"
| bmi < 25 = "Normal"
| bmi < 30 = "Overweight"
| otherwise = "Obese"
where bmi = w / (h * h)
hyp :: Double -> Double -> Double
hyp a b = let a2 = a*a
b2 = b*b
in sqrt (a2 + b2)
These three tools often eliminate explicit if/then/else and make branches and transformations very readable.
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)integersInteger 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
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.
^): Raises the first number to the power of the second number. This executes several hidden multiplication operations and returns the final result.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
fromIntegral or fromDouble.Prelude> 5.2 + fromIntegral (div 7 3)
7.2
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:
++ operator Prelude Data.Char> "hello, " ++ "world"
"hello, world"
Prelude Data.Char> reverse "hello"
"olleh"
Prelude Data.Char> reverse "radar"
"radar"
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.
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")
Organize code with modules and imports in Haskell:
-- File: src/Lib.hs
module Lib (sayTwice, hyp) where
sayTwice :: String -> String
sayTwice s = s ++ s
hyp :: Double -> Double -> Double
hyp a b = sqrt (a*a + b*b)
-- File: app/Main.hs
module Main where
import Lib (sayTwice, hyp)
main :: IO ()
main = do
putStrLn (sayTwice "hello")
print (hyp 3 4)
Add the package name under dependencies in package.yaml, then run:
stack build
Edit the .cabal file under build-depends, then run:
cabal build
To format text with printf, add the printf package (or simply import it if it’s already part of your base distribution):
import Text.Printf (printf)
main = putStrLn (printf "Pi ~ %.2f" pi)
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.
Two list pillars are comprehensions and higher-order traversals:
Two list pillars in Haskell are comprehensions and higher-order traversals.
-- List comprehension: squares of even numbers up to 10
squaresOfEvens :: [Int]
squaresOfEvens = [ x * x | x <- [1..10], even x ]
-- Map / filter / fold:
incAll :: [Int] -> [Int]
incAll = map (+1)
onlyOdds :: [Int] -> [Int]
onlyOdds = filter odd
sumAll :: [Int] -> Int
sumAll = foldr (+) 0
Think of map as “transform each”,
filter as “keep those that…”,
and fold[r/l] as “reduce to a single value.”
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
Haskell’s standard algebraic types model absence and errors explicitly.
-- Maybe for optional values:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
-- Either for success (Right) or error (Left):
parsePositive :: Int -> Either String Int
parsePositive n
| n >= 0 = Right n
| otherwise = Left "negative not allowed"
-- Derive common typeclass instances to get equality, ordering, and printable output for free:
data Point = Point { x :: Int, y :: Int }
deriving (Eq, Ord, Show, Read) -- now you can compare, print, and read
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.
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.
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
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.
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.
Pure code stays pure; side effects happen in IO.
greet :: IO ()
greet = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
import System.IO
copyFileUpper :: FilePath -> FilePath -> IO ()
copyFileUpper src dst = do
contents <- readFile src
writeFile dst (map toUpper contents)
Keep as much logic pure as possible, and call it from tiny IO wrappers.
Haskell culture favors property-based testing.
With QuickCheck, you describe invariants; it generates many random cases.
-- package: QuickCheck
import Test.QuickCheck
prop_reverseInvolutive :: [Int] -> Bool
prop_reverseInvolutive xs = reverse (reverse xs) == xs
main :: IO ()
main = quickCheck prop_reverseInvolutive
This approach catches edge cases you may not think to write as explicit examples.
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:
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!