1

Functions as Expressions

Why this matters

Haskell asks a different question: what value does this expression describe? Start with one small function, and you can explore that shift without installing anything.

🎯 You will learn to

  • Analyze how function calls group with arithmetic
  • Modify a pure function while keeping output in main

This is Part 1 of a three-part path. You need functions, conditionals, and lists from Python or C++; you do not need any Haskell. Plan for 90–110 minutes, and take a break after Step 5.

A tiny worked example

square :: Int -> Int
square x = x * x

:: introduces a type signature: an Int goes in and an Int comes out. = defines the result; it does not assign a new value to x. The body is an expression, so there is no return statement. Call the function with square 3. Names of functions and bindings start with a lowercase letter; type names such as Int start uppercase. -- starts a comment.

Function application binds more tightly than arithmetic: square 3 + 1 means (square 3) + 1. Use parentheses when an argument is itself an expression. f x y groups as (f x) y; that grouping rule does not specify which expression Haskell evaluates first.

Thinking time!

The starter’s nextSquare 3 is meant to square the next integer. Will it print 10, 16, or a type error? Write down your prediction, then choose Run.

Compare after running

It prints 10: the starter squares 3, then adds 1. To square the next integer, the addition must be part of the argument.

Your challenge

Repair nextSquare n to return the square of n + 1 for any Int in the small ranges used here. Try a negative number and zero before choosing Test My Work. Explain why the same definition works for both.

main = print (...) is the supplied display harness. IO () marks an input/output action; () is its result type. Your function computes a value, and main displays it. The browser uses MicroHs, a compact Haskell implementation; course-style definitions go in Main.hs, not interpreter commands such as :load.

A failing check is information about one input, not a verdict on your ability. Read its description, make a prediction, and change one idea.

Starter files
Main.hs
module Main where

square :: Int -> Int
square x = x * x

nextSquare :: Int -> Int
nextSquare n = square n + 1

main :: IO ()
main = print (nextSquare 3)
2

Types and Numeric Boundaries

Why this matters

A program can compile and still throw away the information you need. Types help you decide what a value can represent, but you still choose the operation.

🎯 You will learn to

  • Apply numeric conversion before fractional division
  • Distinguish type inference from runtime type changes

Int represents bounded whole numbers, Integer supports arbitrary precision, and Double represents approximate floating-point numbers. Bool has the values True and False. 'x' is a Char, while "x" is a String. A type is part of the program even when its signature is omitted: Haskell infers types before executing your code.

div performs integral division. Write div 7 2 or use backticks: 7 `div` 2. Both give 3. / is fractional division, so convert an existing Int with fromIntegral when a fractional result is needed. For example, fromIntegral 3 * 2.5 :: Double gives 7.5. Numeric literals can adapt to an expected numeric type; existing Int values do not silently become Double values.

Thinking time!

A team splits seven points between two players. The starter converts its answer to Double. Does that restore the half point, yielding 3.5, or does it produce 3.0? Predict, then run.

Where the fraction went

Integral division already produced 3. Converting that result gives 3.0; it cannot recover information discarded earlier.

Your challenge

Repair pointsPerPlayer total players so it returns the fractional share. Assume total >= 0 and players > 0. Use the provided types; the checks allow floating-point rounding error.

For a short debugging experiment, replace an argument in main with True and run. Read the type error, explain the mismatch, then undo that experiment before testing your work. Type errors and incorrect numerical results are two different kinds of feedback.

Starter files
Main.hs
module Main where
pointsPerPlayer :: Int -> Int -> Double
pointsPerPlayer total players = fromIntegral (total `div` players)
main :: IO ()
main = print (pointsPerPlayer 7 2)
3

Conditional Values and Guards

Why this matters

Your program often has to choose between several answers. In Haskell, a conditional chooses a value, and the order of overlapping rules matters.

🎯 You will learn to

  • Analyze which guard handles a boundary value
  • Create a complete classification function

An if is an expression: if score >= 60 then "Pass" else "Retry". It needs both branches, and their types must agree. Comparisons use ==, /=, <, <=, >, and >=; Boolean combinations use && and ||. Notice the distinction between defining with = and comparing with ==.

Guards give a readable form for several conditions:

ticket age
  | age < 12 = "Child"
  | age < 18 = "Teen"
  | otherwise = "Adult"

Read from the top. The first true guard supplies the result. otherwise is the Boolean value True, so it provides a final fallback.

Thinking time!

The starter has a rule for scores of at least 90. Will rankScore 95 return "Gold" or "Silver"? Name the first matching guard before running.

Your challenge

Repair rankScore for this complete rule:

  • Below 60: "Practice"
  • From 60 through 89: "Silver"
  • At least 90: "Gold"

The function accepts any Int, including values outside the usual score range. Test 59, 60, 89, and 90 yourself. A large sample such as 95 tells you less about the exact threshold than 89 and 90 together. Guards or an equivalent nested if are both valid.

Starter files
Main.hs
module Main where
rankScore :: Int -> String
rankScore score
  | score >= 60 = "Silver"
  | score >= 90 = "Gold"
  | otherwise = "Practice"
main :: IO ()
main = print (rankScore 95)
4

Local Bindings

Why this matters

A long expression can hide a simple idea. Local names let you explain the pieces without turning them into mutable variables.

🎯 You will learn to

  • Apply let and where to name intermediate results
  • Analyze a calculation as a set of relationships

A let expression binds names and uses them after in:

perimeter side =
  let edges = 4
  in edges * side

A where clause attaches local definitions to an equation, including its guards:

perimeter side = edges * side
  where edges = 4

Keep definitions in the same block aligned; indentation is meaningful. Both forms can define helper functions as well as data. A helper can refer to the surrounding function’s parameters. Neither form is a sequence of assignments: let x = x + 1 in x is a recursive definition, not an increment operation.

Thinking time!

Four snacks cost 8 points each. A group order of at least four snacks gets a discount of one quarter of the subtotal, rounded down. Delivery costs 3 points. Should the total be 24, 27, or 35? Work through each named amount, then run the starter and locate the missing relationship.

Your challenge

Repair snackBill count unitCost fee to charge the subtotal minus the discount, plus the fee once. Inputs are nonnegative integers. The discount applies when count >= 4; even an empty order retains the supplied fee. Keep intermediate names that make the rule readable.

After the checks pass, rewrite your definition using let ... in instead of where and run the checks again. Explain why moving the definitions on the page does not change the answer.

Starter files
Main.hs
module Main where
snackBill :: Int -> Int -> Int -> Int
snackBill count unitCost fee = subtotal - discount
  where
    subtotal = count * unitCost
    discount = if count >= 4 then subtotal `div` 4 else 0
main :: IO ()
main = print (snackBill 4 8 3)
5

Tuples and Type Variables

Why this matters

A score usually belongs to someone. A tuple keeps related values together while allowing each position to have its own type.

🎯 You will learn to

  • Analyze tuple shapes and polymorphic type variables
  • Create a new tuple that preserves one field and changes another

("Mina", 40) has type (String, Int) when its score is an Int. Tuple size and position types are fixed: a pair is different from a triple. A tuple parameter is one argument, even though it contains several components.

swapPair :: (a, b) -> (b, a)
swapPair (left, right) = (right, left)

(left, right) is a pattern that names the two components. Lowercase type variables a and b describe arbitrary types. Reusing the same letter requires the same type; different letters permit different types but do not require them to differ. Thus swapPair works for both (True, 'x') and (3, 4).

Thinking time!

The starter defines original = ("Mina", 40) and calls addBonus 5 original. After the repair, should printing original give 40 or 45 as its score? Commit to an answer before you change the function.

Your challenge

Implement addBonus bonus (name, score) so it returns the same name paired with score + bonus. Bonuses may be negative. Preserve the original pair; creating an updated result does not mutate it.

Run the starter, repair it, and run again. The supplied do block sequences two print actions so you can compare the new pair and the original. For now, leave that display harness alone.

Before moving on, explain (a, a) versus (a, b) without looking back. This is a good stopping point: take a break, then recall that distinction before starting lists.

Starter files
Main.hs
module Main where
swapPair :: (a, b) -> (b, a)
swapPair (left, right) = (right, left)
addBonus :: Int -> (String, Int) -> (String, Int)
addBonus bonus (name, score) = (name, score)
original :: (String, Int)
original = ("Mina", 40)
main :: IO ()
main = do
  print (addBonus 5 original)
  print original
6

Lists and Strings

Why this matters

Lists describe collections whose length can vary. Their element type stays consistent, which lets one operation work for songs, numbers, or characters.

🎯 You will learn to

  • Distinguish cons from list concatenation
  • Apply a polymorphic list transformation to strings and other lists

[2, 4, 6] is a list of numbers; ["tea", "mochi"] is a list of strings. Unlike tuples, every element of one list has the same type. [] is empty. String is another name for [Char]: "hi" and ['h', 'i'] represent the same string.

x : xs puts one element at the front of a list. xs ++ ys joins two lists, preserving their order. For example, 1 : [2,3] and [1] ++ [2,3] both give [1,2,3]. "tea" : ["mochi"] is a list of strings; "tea" ++ "mochi" is one string.

take 2 xs keeps up to the first two items; drop 2 xs removes up to the first two. length xs counts items, null xs tests whether the list is empty, and x `elem` xs tests whether it contains x. head xs returns the first item and tail xs returns the remaining list; both require a nonempty list. xs !! i selects an item at a valid zero-based index. Enumerations such as [1..4] and [2,4..10] include the endpoint when the step reaches it.

Thinking time!

What are the lengths of "tea", ["tea"], and []? Predict 3/1/0 or 1/3/0, then use main to check. Explain what counts as an element.

Your challenge

Implement bookend item items: place item at both ends, preserving every existing element in its original order. Empty input still gets two copies of the item. The type [a] should work for any element type.

Try bookend '!' "hi" and bookend "intro" ["song"]. Predict each result’s type before running. The original input list stays unchanged.

Starter files
Main.hs
module Main where
bookend :: a -> [a] -> [a]
bookend item items = items
main :: IO ()
main = do
  print (length "tea", length ["tea"], length ([] :: [Int]))
  print (bookend '!' "hi")
  print (bookend "intro" ["song"])
7

Complete List Patterns

Why this matters

A list might be empty, short, or much longer than your example. Patterns expose those shapes directly, so missing cases become easier to spot.

🎯 You will learn to

  • Analyze exact-length and nonempty-list patterns
  • Create a function that handles every list length

Compare a list’s construction with its decomposition. x : xs constructs a list; the parameter pattern (x:xs) extracts its first element into x and its remaining list into xs.

firstOr fallback [] = fallback
firstOr fallback (x:xs) = x

Equations are tried from top to bottom. [] matches an empty list, [x] exactly one element, [x,y] exactly two, and (x:y:rest) at least two. _ matches a value you do not need to name.

A case expression selects a pattern inside another expression:

firstOr fallback values = case values of
  [] -> fallback
  x:_ -> x

Here alternatives use -> rather than the = of function equations.

Thinking time!

Which inputs match [x,y]: [5], [5,7], or [5,7,9]? Which match (x:y:_)? Predict the difference before trying examples.

Your challenge

Implement secondOr fallback values. Return the second element when there are at least two; otherwise return the supplied fallback. Preserve polymorphism: it must work for numbers, strings, and characters.

The starter always returns the fallback. Run it, then replace that single catch-all with complete cases. Use either function equations or case. Explain why matching exactly two elements would miss a valid input.

Starter files
Main.hs
module Main where
firstOr :: a -> [a] -> a
firstOr fallback [] = fallback
firstOr fallback (x:xs) = x
secondOr :: a -> [a] -> a
secondOr fallback values = fallback
main :: IO ()
main = print (secondOr "silence" ["intro", "chorus", "outro"])
8

Structural Recursion

Why this matters

You already know how to process one item. Recursion connects that small decision to the rest of a collection, without a loop counter to mutate.

🎯 You will learn to

  • Analyze the base case and progress of a recursive definition
  • Create a list reduction from a per-element decision

Here is a complete recursive sum:

total [] = 0
total (x:xs) = x + total xs

The empty list supplies an answer immediately. The nonempty case combines one element with the result for a smaller input. For [4,2], expand 4 + total [2], then 4 + (2 + total []), and finally 4 + (2 + 0). Each call has its own parameter bindings.

Thinking time!

Suppose a counter has base case count [] = 1 and adds one per element. Would a list of three items produce 3 or 4? Explain where the extra unit comes from before running anything.

Your challenge

Implement countAtLeast threshold scores: count every score greater than or equal to the threshold, including repeated scores. Return zero for an empty list. Scores and the threshold can be negative.

Run the starter, which currently reports zero for every input. Build from the sum example: decide whether the first score contributes zero or one, then combine it with the result for the tail. Recursion is the practice focus; any behaviorally equivalent implementation passes.

Before testing, trace [60,59,60] with threshold 60. Name the input that gets smaller and the value supplied by the base case. If a program keeps running, use Stop, then check whether the recursive call receives the tail rather than the original list.

Starter files
Main.hs
module Main where
total :: [Int] -> Int
total [] = 0
total (x:xs) = x + total xs
countAtLeast :: Int -> [Int] -> Int
countAtLeast threshold scores = 0
main :: IO ()
main = print (countAtLeast 60 [60, 59, 60])
9

List Comprehensions

Why this matters

Sometimes a collection is easiest to describe by the candidates you allow. A comprehension names those candidates, filters them, and builds a result.

🎯 You will learn to

  • Analyze generator order and inclusive numeric ranges
  • Create a list of pairs satisfying a stated condition
[2 * n | n <- [1..4], n > 2]

Read it as: take each n from 1 through 4, keep those greater than 2, and emit 2 * n. The answer is [6,8]. <- introduces a generator; a Boolean qualifier filters candidates. The expression before | constructs each output element.

With two generators, the right one varies fastest: [(x,y) | x <- [1,2], y <- [3,4]] produces [(1,3),(1,4),(2,3),(2,4)], rather than pairing positions. Later generators may depend on earlier values, such as y <- [x..4].

Thinking time!

Predict [(x,y) | x <- [1,2], y <- [x..2]]. Is (2,1) included? Is (2,2) included? Explain each decision, then temporarily print the expression to compare.

Your challenge

Implement snackPairs limit target: return every pair (a,b) with 1 <= a <= b <= limit and a + b == target. Think of a and b as snack prices. Equal prices are allowed; reversed duplicates are not. Order by increasing a, then increasing b. For a nonpositive limit, return the empty list. For example, snackPairs 5 6 is [(1,5),(2,4),(3,3)].

The starter already generates candidates but keeps too many. Repair its qualification. Comprehensions are the practice focus; equivalent recursive solutions are accepted. Explain which part controls shape, which controls order, and which enforces the budget total.

Starter files
Main.hs
module Main where
snackPairs :: Int -> Int -> [(Int, Int)]
snackPairs limit target = [(a,b) | a <- [1..limit], b <- [a..limit]]
main :: IO ()
main = print (snackPairs 5 6)
10

The Snack Budget Challenge

Why this matters

A technique is useful when you can recognize when to use it. This challenge combines the pieces without giving you the next line to write.

🎯 You will learn to

  • Create a recursive solution from a behavioral specification
  • Evaluate a stopping rule using contrasting inputs

Your challenge

Implement takeBudget budget costs. Walk the snack prices in the given order and return the longest prefix you can afford in total. Once the next snack costs more than the remaining budget, stop. Do not skip it to buy cheaper snacks later. Budgets and prices are nonnegative integers, and free snacks are allowed.

Examples of the specification:

  • takeBudget 7 [3,4,1] gives [3,4].
  • takeBudget 5 [6,1,1] gives [].
  • takeBudget 0 [0,0,2] gives [0,0].

Thinking time!

Before editing, predict takeBudget 5 [2,4,1]. Choose [2], [2,1], or [2,4], and justify it from the word prefix. Run the starter to see which tempting but incomplete strategy it currently follows.

Write your own plan as three cases: empty input, an affordable first snack, and an unaffordable first snack. Decide what information the smaller recursive problem needs. Then implement and test your plan. You may use helpers or library functions; the checks measure the stated behavior. Resist reading a solution before you have tried a complete plan.

A short return visit

Tomorrow, recreate this function without looking at today’s code. Then explain why its stopping rule differs from selecting every individually cheap snack. If you study with someone, exchange one input that would expose the other’s likely bug and explain it.

Continue with Part 2: Functions and Laziness. It turns recurring list patterns into reusable higher-order functions.

Starter files
Main.hs
module Main where
takeBudget :: Int -> [Int] -> [Int]
takeBudget budget costs = [cost | cost <- costs, cost <= budget]
main :: IO ()
main = print (takeBudget 5 [2,4,1])