Beginner

Hello World in Haskell

Your first Haskell program - the classic Hello World example with Docker setup using GHC

Every programming journey starts with Hello World. Let’s write our first Haskell program using GHC (Glasgow Haskell Compiler), the standard Haskell implementation.

The Code

Create a file named hello.hs:

1
2
main :: IO ()
main = putStrLn "Hello, World!"

That’s it! Just two lines for a complete Haskell program.

Understanding the Code

Let’s break down each part:

The Type Signature

1
main :: IO ()
  • main - The name of the function (the entry point)
  • :: - Read as “has type”
  • IO () - The type of the function

The type IO () means:

  • IO - This function performs input/output (a side effect)
  • () - It returns unit (similar to void in other languages)

The Function Definition

1
main = putStrLn "Hello, World!"
  • main = - Defines what main does
  • putStrLn - A function that prints a string followed by a newline
  • "Hello, World!" - The string argument to print

Why Two Lines?

In Haskell, type signatures are optional - the compiler can usually infer them:

1
2
-- This works too!
main = putStrLn "Hello, World!"

However, explicit type signatures are considered good practice because they:

  • Document your intent
  • Help catch errors
  • Make code easier to understand

Running with Docker

The easiest way to run Haskell without installing anything locally is with Docker:

1
2
3
4
5
# Pull the official Haskell image
docker pull haskell:9.6

# Run the program
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc hello.hs

Understanding the Docker Command

  • docker run --rm - Run a container and remove it when done
  • -v $(pwd):/app - Mount the current directory to /app in the container
  • -w /app - Set the working directory to /app
  • haskell:9.6 - Use the official Haskell 9.6 Docker image
  • runghc hello.hs - Interpret and run the Haskell file

Compiling vs Interpreting

The runghc command interprets your program (good for quick testing). For compiled code:

1
docker run --rm -v $(pwd):/app -w /app haskell:9.6 sh -c 'ghc hello.hs && ./hello'

This compiles to a native executable, which runs much faster for real programs.

Expected Output

Hello, World!

Alternative Approaches

Haskell offers several ways to output text:

Using print

1
2
main :: IO ()
main = print "Hello, World!"

Note: print adds quotes around strings in output: "Hello, World!"

Using putStr (no newline)

1
2
main :: IO ()
main = putStr "Hello, World!\n"

Using do notation

For multiple actions:

1
2
3
4
main :: IO ()
main = do
    putStr "Hello, "
    putStrLn "World!"

Using the » operator

Sequence actions without binding results:

1
2
main :: IO ()
main = putStr "Hello, " >> putStrLn "World!"

Understanding IO in Haskell

Haskell is a pure functional language - functions can’t have side effects. So how does putStrLn print to the screen?

The IO Type

IO is a type that represents a computation that may perform side effects:

1
2
putStrLn :: String -> IO ()
--         ^input    ^returns IO action that produces nothing

When you write putStrLn "Hello", you’re not executing a print. You’re creating a value that represents “the action of printing Hello.”

main is Special

The Haskell runtime executes whatever IO action is bound to main. Only main (and functions called by main) actually perform side effects.

1
2
3
4
5
6
7
-- This creates an IO action but doesn't execute it
unused :: IO ()
unused = putStrLn "You'll never see this"

-- Only main is executed
main :: IO ()
main = putStrLn "Hello, World!"

Sequencing with do

The do notation lets you sequence multiple IO actions:

1
2
3
4
5
main :: IO ()
main = do
    putStrLn "First"    -- Execute this action
    putStrLn "Second"   -- Then this one
    putStrLn "Third"    -- Then this one

This desugars to uses of the >> and >>= operators:

1
main = putStrLn "First" >> putStrLn "Second" >> putStrLn "Third"

Function Application

Haskell uses space for function application, not parentheses:

1
2
3
4
5
-- Haskell style
putStrLn "Hello, World!"

-- NOT like other languages
-- putStrLn("Hello, World!")  -- This would be wrong!

When you need to group expressions, use parentheses:

1
2
3
4
5
-- Print the result of a calculation
print (2 + 2)

-- Nested function calls
putStrLn (show (2 + 2))  -- Outputs: 4

Type Inference

Haskell can usually figure out types automatically:

1
2
3
4
5
-- These are all equivalent
main :: IO ()
main = putStrLn "Hello, World!"

main = putStrLn "Hello, World!"  -- Type inferred

You can ask GHCi what type something has:

ghci> :type putStrLn
putStrLn :: String -> IO ()

ghci> :type "Hello"
"Hello" :: String

Common Beginner Mistakes

Missing the Type Signature Context

1
2
3
4
5
6
7
-- Wrong: Can't use putStrLn in a pure function
greet :: String
greet = putStrLn "Hello"  -- Error! putStrLn returns IO (), not String

-- Right: Return a String, don't print
greet :: String
greet = "Hello"

Forgetting IO is a Functor

1
2
3
4
5
6
7
8
9
-- You can't use the result of getLine directly
main = do
    name = getLine  -- Wrong!
    putStrLn name

-- Use <- to "extract" from IO
main = do
    name <- getLine  -- Right!
    putStrLn name

Wrong String Syntax

1
2
3
4
5
6
7
8
-- Double quotes for strings
putStrLn "Hello"

-- Single quotes for characters
putChar 'H'

-- NOT:
-- putStrLn 'Hello'  -- Wrong! Single quotes are for Char

Installing Haskell Locally

If you prefer to run Haskell without Docker:

Using GHCup (Recommended):

1
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

This installs:

  • GHC (the compiler)
  • Cabal (package manager)
  • Stack (build tool)
  • HLS (language server for editors)

macOS with Homebrew:

1
brew install ghc cabal-install

Ubuntu/Debian:

1
sudo apt install ghc

After installation:

1
2
3
4
5
6
# Interpret
runghc hello.hs

# Or compile and run
ghc hello.hs
./hello

The REPL: GHCi

Haskell has an excellent interactive environment:

$ ghci
GHCi, version 9.6.1: https://www.haskell.org/ghc/
Prelude> putStrLn "Hello, World!"
Hello, World!
Prelude> 2 + 2
4
Prelude> :type putStrLn
putStrLn :: String -> IO ()
Prelude> :quit

Useful GHCi commands:

  • :type (:t) - Show the type of an expression
  • :info (:i) - Show info about a type or function
  • :load (:l) - Load a file
  • :reload (:r) - Reload the current file
  • :quit (:q) - Exit GHCi

A Bit of History

Haskell was designed by a committee of researchers in 1990 to unify the various lazy functional languages that existed at the time. It was named after Haskell Curry, a mathematician whose work on combinatory logic laid the foundation for functional programming.

The language has evolved significantly:

  • 1990: Haskell 1.0 released
  • 1996: Haskell 1.3 added monadic I/O
  • 1998: Haskell 98 standardized
  • 2010: Haskell 2010 (current standard)

GHC, developed primarily at the University of Glasgow, has become the standard implementation and continues to add experimental features that often become de facto standards.

Why Haskell for Hello World?

Even in this simple program, you see Haskell’s philosophy:

  1. Types First - The type signature documents what main does
  2. Purity Matters - IO is explicitly marked in the type
  3. Simplicity - No boilerplate, just the essential logic
  4. Safety - The type system ensures you can’t accidentally mix pure and impure code

Next Steps

Continue to Functions and Types to learn about Haskell’s powerful type system and how to define your own functions.

Key Takeaways

  1. main :: IO () declares an IO action as the entry point
  2. putStrLn prints a string with a newline
  3. Type signatures are optional but recommended
  4. runghc interprets Haskell; ghc compiles it
  5. IO is a type that represents side effects
  6. Function application uses space, not parentheses
  7. do notation sequences multiple IO actions

Running Today

All examples can be run using Docker:

docker pull haskell:9.6
Last updated: