Haskell World logo Haskell WorldWrite code, build worlds
Tutorials

Getting Started With Haskell for Complete Beginners

Haskell has a reputation for being difficult to learn. Some of that reputation is deserved - the language makes different assumptions than most popular languages, and getting past those differences takes time.

Getting started with Haskell programming for complete beginners

Haskell has a reputation for being difficult to learn. Some of that reputation is deserved - the language makes different assumptions than most popular languages, and getting past those differences takes time. But the difficulty is front-loaded. The concepts that feel alien at first - types, purity, functional style - become natural with practice, and the understanding you develop working through them changes how you think about programming in any language. This is a practical introduction for someone starting from zero.

Setting up your environment

The easiest way to get a working Haskell environment is through GHCup, the official Haskell toolchain installer. GHCup installs GHC (the Haskell compiler), Cabal (the package manager and build tool), and HLS (the Haskell Language Server, which provides IDE features in editors like VS Code). On a fresh machine, you visit the GHCup website, run the installer command in your terminal, and follow the prompts. The whole process takes about fifteen minutes depending on your internet connection.

For editing, VS Code with the Haskell extension is a good starting point. Once HLS is running, you get type-on-hover, auto-complete, inline error messages, and automatic imports. These features matter more in Haskell than in many other languages because the type information is so rich - seeing the type of a sub-expression while writing code is genuinely informative and saves a lot of guessing.

To start a new project, use Cabal: run cabal init in a new directory, answer a few questions, and you have a project structure with a main module and a test suite. Run cabal build to compile and cabal run to execute. The interactive compiler, GHCi, is available with cabal repl - this is a REPL that evaluates Haskell expressions interactively and is one of the most useful tools for experimenting with code as you learn.

Your first Haskell expressions

Open GHCi and start experimenting. Arithmetic works as you would expect: 2 + 3 gives 5, 10 / 3 gives a decimal. String concatenation uses the ++ operator: "hello" ++ " world" gives "hello world". You can ask GHCi for the type of any expression with :type or :t: :t 42 shows Num p => p, which means 42 is a numeric literal whose exact type is not yet determined.

Functions are defined with a simple syntax. In a file: double x = x * 2. That is a function called double that takes one argument x and returns x times two. No return keyword. No type annotation required (though adding one is good practice). Call it with double 5, not double(5) - function application in Haskell uses spaces, not parentheses. Parentheses group expressions: double (2 + 3) applies double to the result of 2 + 3.

Haskell functions are curried by default. A function of two arguments is actually a function that takes one argument and returns another function. add x y = x + y can be called as add 3 5 to get 8, or as add 3 to get a function that adds 3 to its argument. Partial application - applying a function to fewer arguments than it takes - is natural and widely used.

Types and type signatures

Every expression in Haskell has a type, and the compiler checks that types are used consistently. You do not always need to write type annotations - the compiler can usually infer them - but writing them makes your code clearer and helps the compiler give you better error messages when something is wrong.

Type annotations are written with a double colon: double :: Int -> Int. This says double takes an Int and returns an Int. A function of two arguments: add :: Int -> Int -> Int. The arrow is right-associative, so this is actually Int -> (Int -> Int), which is consistent with currying - add takes an Int and returns a function from Int to Int.

Common types you will encounter immediately: Int and Integer (fixed and arbitrary precision integers), Double (floating point), Bool (True or False), Char (a character, with single quotes), String (a list of Char, with double quotes), and list types written with brackets: [Int] is a list of integers. Tuples group values of possibly different types: (Int, String) is a pair of an integer and a string.

Lists and recursion

Lists are fundamental in Haskell. The empty list is written []. A list with elements is written [1, 2, 3]. The cons operator : prepends an element to a list: 1 : [2, 3] gives [1, 2, 3]. Standard list functions include length, head (first element), tail (all but first), map (apply a function to each element), and filter (keep elements matching a condition).

The map function is worth understanding well. map double [1, 2, 3] applies double to each element and returns [2, 4, 6]. The filter function: filter even [1, 2, 3, 4] returns [2, 4]. These higher-order functions - functions that take other functions as arguments - appear constantly in Haskell code and are part of how iteration is expressed without explicit loops.

Recursion replaces loops in Haskell. A function that computes the sum of a list is written recursively: the sum of an empty list is 0; the sum of a non-empty list is its first element plus the sum of the rest. Pattern matching handles the two cases: sum [] = 0 and sum (x:xs) = x + sum xs. The pattern (x:xs) matches any non-empty list, binding x to the head and xs to the tail.

Algebraic data types

You can define your own types in Haskell. A product type bundles multiple values together: data Point = Point Double Double defines a Point type with an x and y coordinate. Create one with Point 3.0 4.0. Extract the coordinates with pattern matching: getX (Point x _) = x.

Sum types have multiple variants, each with their own data. A classic example is Maybe, which is already in Haskell's standard library: data Maybe a = Nothing | Just a. A Maybe Int is either Nothing (no value) or Just some Int. This is Haskell's approach to the null value problem: instead of having null values that can appear anywhere, you use Maybe to explicitly represent the possibility of absence.

Pattern matching on sum types is exhaustive. When you match on a Maybe, you must handle both Nothing and Just. The compiler warns if you miss a case. This turns what would be a null pointer exception in other languages into a compile-time check: you cannot forget to handle the absence case because the compiler will not let you.

The IO monad and your first program

Haskell keeps pure computation strictly separate from interactions with the outside world. The IO monad is the mechanism for this. A function with type IO () is an action that performs some IO and returns nothing meaningful. A function with type IO Int performs some IO and returns an Int.

The do notation makes working with IO comfortable. A simple program that reads a line and prints it back looks like this: main = do { line <- getLine; putStrLn line }. The <- binds the result of getLine to the name line, making the String value available for use. putStrLn prints a string followed by a newline. The do block sequences IO actions, executing them in order.

The important mental model is that IO actions are values. Main is a value of type IO () that the Haskell runtime executes. Writing Haskell programs means building up IO actions from smaller IO actions and pure functions. The pure computation - the logic - lives in ordinary functions. The IO - reading input, writing output, accessing files - lives in IO-typed functions. Keeping them separate is not a restriction but a clarity improvement: you can always tell, from the type, whether a function can have side effects.

Where to go next

The best next step is to work through a structured resource while building something. "Learn You a Haskell for Great Good" is free online and covers the foundational concepts with plenty of examples. The Haskell Book is more comprehensive and suitable for developers who want a thorough treatment. Exercism's Haskell track provides exercises with community feedback, which helps catch habits that are technically correct but not idiomatic.

The concepts that will challenge you next are typeclasses, which generalize interfaces; the full monad hierarchy, which generalizes IO to other kinds of effects; and lazy evaluation, which changes how you think about computation order. Each of these takes time to internalize but repays the investment. The point where Haskell stops feeling strange and starts feeling like the natural way to express solutions is a real inflection point, and reaching it is worth the effort it takes to get there.

AS
Adil Sato

Adil Sato teaches programming concepts at a community college and writes online guides for the questions students ask most in the second week of class, not the first. His focus is on making type systems, monads, and functional patterns understandable without reducing them to metaphors that break down the moment you try to use them for real work.

More posts by Adil

More from the blog