Part 1 of Haskell from First Principles.
Haskell is a useful language to approach as a mathematical language: learn how expressions are constructed, what they mean, and which types they have. No background in functional programming or category theory is needed for this series.
This first block establishes the vocabulary used throughout: syntax describes what can be written, semantics describes its meaning, and types describe how expressions fit together.
Definitions in haskell code blocks belong in a source file such as Block1.hs. Examples with a ghci> prompt are interactive sessions; type only the text after the prompt. Separate examples sometimes reuse a function name, so treat them as alternatives rather than combining every definition into one file.
1.1 The fundamental idea: Haskell is expression-oriented
An imperative program often describes instructions that update state. For example, Python permits reassignment:
x = 3
x = x + 1
print(x)Here x changes from 3 to 4. Haskell’s central building blocks are expressions that denote values:
| Expression | Result |
|---|---|
3 | 3 |
3 + 5 | 8 |
(3 + 5) * 2 | 16 |
10 > 3 | True |
3 == 4 | False |
"Hello" | "Hello" |
For these examples, ordinary mathematical reasoning works:
Evaluating a pure expression does not update hidden mutable state. This makes substitution a useful way to reason about a program. The examples here all produce results, but a well-typed Haskell expression can also fail or run forever; having a type does not guarantee a terminating result.
1.2 Defining values: =
In a source file, write definitions like these:
x = 3
y = 5
z = x + yThe symbol = establishes a binding: the name x denotes 3, and z denotes the result of x + y. It does not mean “store a new value in a mutable variable.”
The following is an intentionally invalid source-file example:
x = 3
x = 4These are conflicting definitions in the same scope, rather than two successive updates. Later, local scopes will allow a new binding to shadow an outer one, but that is a different mechanism.
Keep these symbols separate:
| Symbol | Purpose |
|---|---|
= | Define a name |
== | Compare values for equality |
-- | Begin a line comment |
After loading the definitions into GHCi:
ghci> x == 3
True
ghci> x == 4
False1.3 Defining functions
A function definition names a calculation in terms of its parameters:
square x = x * xMathematically, this corresponds to . Compare the same idea in Python:
def square(x):
return x * xHaskell needs neither parentheses and commas around these parameters nor an explicit return.
Part of square x = x * x | Role |
|---|---|
square | Function name |
x | Parameter |
x * x | Body expression |
Functions can have several parameters:
add x y = x + y
multiply x y = x * y
average x y = (x + y) / 2The simplified syntax is:
functionName parameter1 parameter2 ... = expressionThe body denotes the result. These arithmetic functions also illustrate that the permitted input types depend on the operations used: / requires a fractional numeric type, which we will examine in Block 2.
1.4 Function application: whitespace is important
Mathematics commonly writes . Haskell normally writes f x. Applying a function uses whitespace:
ghci> square 5
25
ghci> add 3 4
7
ghci> square (add 2 3)
25In the last example, parentheses group the entire expression add 2 3 as the argument to square:
square (add 2 3)
= square 5
= 5 * 5
= 25Function application binds more tightly than ordinary arithmetic operators. Consequently:
| Expression | Grouping | Result |
|---|---|---|
square 3 + 4 | (square 3) + 4 | 13 |
square (3 + 4) | square (3 + 4) | 49 |
Parentheses control grouping; they are not a required part of a function-call syntax. For instance, square(5) also parses as an application, but square 5 is the usual style. A comma-separated pair such as (3, 4) is a single tuple argument, not the same thing as the two arguments in add 3 4.
1.5 Evaluating functions by substitution
Consider these source-file definitions:
double x = x + x
square x = x * x
f x = square (double x)We can understand f 3 by substituting definitions:
f 3
= square (double 3)
= square (3 + 3)
= square 6
= 6 * 6
= 36This is equational reasoning: replace an expression with another that has the same meaning.
Haskell uses lazy evaluation, so this explanation is not a promise about the precise order of machine operations. An argument is evaluated when its value is needed. For these pure, terminating calculations, substitution still gives a reliable explanation of the result.
1.6 Type signatures: :: and ->
Haskell is statically typed. A function’s type can be written explicitly above its definition:
square :: Int -> Int
square x = x * xThe signature says that square takes an Int and returns an Int:
| Syntax | Meaning |
|---|---|
:: | “Has type” |
Int | A bounded integer type |
-> | Constructs a function type |
Int is convenient for introductory examples, but its range is finite. Arithmetic can overflow; it is not identical to the mathematical integers . Use Integer when arbitrary-precision integer arithmetic is required.
A function with two integer arguments has this signature:
add :: Int -> Int -> Int
add x y = x + yFor now, read it as “two integer inputs, one integer output.” More precisely, Int -> Int -> Int means Int -> (Int -> Int): one argument produces a new function that accepts the next argument. This is currying, introduced in Block 2 and developed further in Block 6.
Type signatures are often optional because the compiler can infer them. Writing them is still valuable: they document intent and help explain errors.
1.7 A first look at Haskell’s grammar
The pieces learned so far can be summarized with a simplified grammar:
Expression :=
literal
| variable
| (Expression)
| Expression operator Expression
| Expression Expression
Definition :=
name = Expression
| name parameter1 ... parameterN = Expression
TypeSignature :=
name :: TypeThis is a teaching sketch, not the complete formal grammar. It does not specify all precedence, associativity, naming, or layout rules.
Its central idea is that complex expressions are built from simpler expressions and function applications:
f x = x + 1
g x = x * 2
h x = f (g x)Here h combines two ordinary functions. No mutable intermediate variable is necessary.
1.8 Trying the examples in GHCi
GHCi is GHC’s interactive environment. If Haskell tooling is not installed, the official GHCup website provides installation guidance. Once available, start it from a terminal:
ghciThen evaluate expressions and introduce interactive bindings:
ghci> 3 + 5
8
ghci> let square x = x * x
ghci> square 5
25
ghci> let add x y = x + y
ghci> add 3 4
7
ghci> :type square
square :: Num a => a -> aThe :type command asks for a type without evaluating the expression. Because this interactive square has no restrictive signature, GHCi infers a type more general than Int -> Int. The constraint Num a => says that a must support the relevant numeric operations; Block 2 explains the notation.
Interactive let bindings do not need in. A source-file top-level definition is simply square x = x * x. Modern GHCi also accepts declarations without the leading let; the form above makes the distinction easy to see. See the official GHCi guide.
For a source-file workflow, save definitions in Block1.hs, then use:
ghci> :load Block1.hs
ghci> square 5
25After editing the file, :reload loads the new definitions. Expressions such as square 5 go at the prompt; they are not standalone top-level declarations in the file.
1.9 Exercises
Try each exercise before opening its solution.
Exercise 1: combine two functions
Given these definitions:
double x = x * 2
square x = x * xWhat does the GHCi expression square (double 3) + 2 evaluate to?
Show solution
38: double 3 is 6, square 6 is 36, and 36 + 2 is 38.
Exercise 2: recognize grouping
Are these GHCi expressions equivalent?
square (3 + 4)
square 3 + 4Show solution
No. The first evaluates to 49; the second means (square 3) + 4 and evaluates to 13.
Exercise 3: define a cube function
Write a source-file definition of cube with type Int -> Int that returns its argument multiplied by itself three times.
Show solution
cube :: Int -> Int
cube x = x * x * xFor example, cube 3 evaluates to 27. As with any Int arithmetic, sufficiently large results can overflow.
The foundation is now in place: expressions denote results, bindings define names, whitespace applies functions, and types describe how inputs and outputs fit together. Next comes a closer look at the type system and the types the compiler can infer.
Adapted from Block 1 of the original GPT-produced Haskell tutorial.