In most languages, variables are containers that hold values you can change at will. Haskell takes a fundamentally different approach. As a purely functional language, Haskell has no mutable variables — instead, you create bindings that permanently associate a name with a value. This might sound limiting, but it turns out to be remarkably powerful.
Haskell’s type system is one of the most expressive in any mainstream language. It catches entire categories of bugs at compile time, yet rarely requires you to write type annotations thanks to type inference. The compiler figures out the types for you, and if something doesn’t fit, it tells you before your program ever runs.
In this tutorial, you’ll learn how Haskell handles bindings and types — from basic values like integers and strings to algebraic data types that let you model complex domains with precision. You’ll see how immutability and strong typing work together to produce code that is both safe and elegant.
Immutable Bindings and Basic Types
Haskell’s basic types include Int (fixed-precision integer), Integer (arbitrary-precision integer), Float, Double, Bool, Char, and String (which is just a list of Char). Bindings are introduced with let (in expressions) or at the top level of a module.
Create a file named variables.hs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| module Main where
main :: IO ()
main = do
-- let bindings associate names with values
let x = 42 -- Int (inferred)
let name = "Haskell" -- String (which is [Char])
let pi' = 3.14159 -- Double (inferred)
let active = True -- Bool
let letter = 'H' -- Char
putStrLn "-- Basic Bindings --"
putStrLn ("x = " ++ show x)
putStrLn ("name = " ++ name)
putStrLn ("pi' = " ++ show pi')
putStrLn ("active = " ++ show active)
putStrLn ("letter = " ++ show letter)
-- Type annotations can be explicit
let year :: Int
year = 2026
putStrLn ("year = " ++ show year)
-- Integer supports arbitrary precision
let big :: Integer
big = 2 ^ 100
putStrLn ("2^100 = " ++ show big)
-- Bindings are immutable: you cannot reassign x
-- let x = 99 -- This creates a NEW binding that shadows the old one
let x' = x + 1
putStrLn ("x' = x + 1 = " ++ show x')
-- where clauses are another way to define bindings
putStrLn ("circumference = " ++ show circumference)
where
circumference = 2 * 3.14159 * 10.0
|
Type Inference and Type Signatures
One of Haskell’s strengths is that you rarely need to write types explicitly — the compiler infers them. However, writing type signatures is considered good practice because they serve as documentation and help catch errors earlier.
Create a file named variables_types.hs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| module Main where
-- Top-level bindings with explicit type signatures
radius :: Double
radius = 5.0
label :: String
label = "circle"
-- Functions have types too: input -> output
area :: Double -> Double
area r = pi * r * r
-- Multiple parameters are curried
add :: Int -> Int -> Int
add a b = a + b
-- Polymorphic types work with any type in a type class
showPair :: (Show a, Show b) => a -> b -> String
showPair x y = show x ++ " and " ++ show y
main :: IO ()
main = do
putStrLn "-- Type Signatures --"
putStrLn ("radius = " ++ show radius)
putStrLn ("label = " ++ label)
putStrLn ("area radius = " ++ show (area radius))
putStrLn ("add 3 7 = " ++ show (add 3 7))
putStrLn ("showPair 42 True = " ++ showPair 42 True)
-- Type conversions are explicit in Haskell
putStrLn "\n-- Type Conversions --"
let intVal = 42 :: Int
let doubleVal = fromIntegral intVal :: Double
putStrLn ("Int to Double: " ++ show doubleVal)
let floored = floor 3.7 :: Int
putStrLn ("floor 3.7 = " ++ show floored)
let rounded = round 3.5 :: Int
putStrLn ("round 3.5 = " ++ show rounded)
-- show converts any showable value to String
let numStr = show 123
putStrLn ("show 123 = " ++ numStr)
-- read parses a String into a type
let parsed = read "456" :: Int
putStrLn ("read \"456\" = " ++ show parsed)
|
Algebraic Data Types and Pattern Matching
Haskell’s algebraic data types let you define custom types by combining simpler ones. Sum types (with |) represent alternatives, and product types group multiple fields together. Pattern matching lets you deconstruct these types elegantly.
Create a file named variables_adt.hs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| module Main where
-- Sum type: a Shape is either a Circle OR a Rectangle
data Shape = Circle Double
| Rectangle Double Double
deriving (Show)
-- Pattern matching on custom types
describeShape :: Shape -> String
describeShape (Circle r) =
"Circle with radius " ++ show r
describeShape (Rectangle w h) =
"Rectangle " ++ show w ++ " x " ++ show h
shapeArea :: Shape -> Double
shapeArea (Circle r) = pi * r * r
shapeArea (Rectangle w h) = w * h
-- Maybe represents optional values (no null in Haskell!)
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)
-- Tuples group values of different types
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
-- Lists hold values of the same type
describelist :: [a] -> String
describelist [] = "empty"
describelist [_] = "one element"
describelist _ = "multiple elements"
main :: IO ()
main = do
putStrLn "-- Algebraic Data Types --"
let c = Circle 5.0
let r = Rectangle 3.0 4.0
putStrLn (describeShape c)
putStrLn (" area = " ++ show (shapeArea c))
putStrLn (describeShape r)
putStrLn (" area = " ++ show (shapeArea r))
putStrLn "\n-- Maybe (No Null!) --"
putStrLn ("10 / 3 = " ++ show (safeDivide 10 3))
putStrLn ("10 / 0 = " ++ show (safeDivide 10 0))
putStrLn "\n-- Tuples --"
let pair = (1, "hello")
putStrLn ("pair = " ++ show pair)
putStrLn ("swap = " ++ show (swap pair))
putStrLn "\n-- Lists --"
let nums = [1, 2, 3, 4, 5] :: [Int]
putStrLn ("nums = " ++ show nums)
putStrLn ("head = " ++ show (head nums))
putStrLn ("tail = " ++ show (tail nums))
putStrLn ("length = " ++ show (length nums))
putStrLn ("[] is " ++ describelist ([] :: [Int]))
putStrLn ("[1] is " ++ describelist [1 :: Int])
putStrLn ("[1,2,3] is " ++ describelist [1, 2, 3 :: Int])
|
Running with Docker
1
2
3
4
5
6
7
8
9
10
11
| # Pull the official image
docker pull haskell:9.6
# Run the basic bindings example
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc variables.hs
# Run the types example
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc variables_types.hs
# Run the algebraic data types example
docker run --rm -v $(pwd):/app -w /app haskell:9.6 runghc variables_adt.hs
|
Expected Output
Running variables.hs:
-- Basic Bindings --
x = 42
name = Haskell
pi' = 3.14159
active = True
letter = 'H'
year = 2026
2^100 = 1267650600228229401496703205376
x' = x + 1 = 43
circumference = 62.8318
Running variables_types.hs:
-- Type Signatures --
radius = 5.0
label = circle
area radius = 78.53981633974483
add 3 7 = 10
showPair 42 True = 42 and True
-- Type Conversions --
Int to Double: 42.0
floor 3.7 = 3
round 3.5 = 4
show 123 = 123
read "456" = 456
Running variables_adt.hs:
-- Algebraic Data Types --
Circle with radius 5.0
area = 78.53981633974483
Rectangle 3.0 x 4.0
area = 12.0
-- Maybe (No Null!) --
10 / 3 = Just 3.3333333333333335
10 / 0 = Nothing
-- Tuples --
pair = (1,"hello")
swap = ("hello",1)
-- Lists --
nums = [1,2,3,4,5]
head = 1
tail = [2,3,4,5]
length = 5
[] is empty
[1] is one element
[1,2,3] is multiple elements
Key Concepts
- Bindings, not variables — Haskell names are bound to values permanently. There is no assignment operator that changes a binding’s value after it is created.
- Type inference — The compiler deduces types automatically, but explicit type signatures are recommended for top-level definitions as documentation.
- Explicit conversions — Haskell never implicitly converts between types. Use
fromIntegral, floor, round, show, and read to convert explicitly. - Algebraic data types — Sum types (
|) and product types let you model domains precisely, and the compiler ensures you handle every case. - Maybe instead of null — Haskell has no null. The
Maybe type (Just value or Nothing) makes optionality explicit and type-safe. - Tuples and lists — Tuples hold a fixed number of values of different types; lists hold any number of values of the same type.
- Pattern matching — Destructure values directly in function definitions, replacing chains of if-else with clear, exhaustive case handling.
- Immutability enables reasoning — When values never change, you can substitute equals for equals anywhere in your code, making programs easier to understand and refactor.
Comments
Loading comments...
Leave a Comment