Skip to content

HW 2: FUNctional programming

Time to write some real Haskell. This assignment has you implementing functions that build on Module 02.1 and Module 02.2 — recursion, pattern matching, and higher-order functions — and ends with a neat trick: building arithmetic itself (addition, multiplication, exponentiation) out of nothing but a single function and a number.

How you'll get materials and submit this assignment

Getting the starter code and turning in your work follows the same process as every homework this semester — see The Homework Workflow for the full walkthrough. In short: from the hw2 template repo, make your own private copy, clone it to the course server, and submit your test output and code on Gradescope. The rest of this page covers what the assignment actually asks of you.

What You'll Do

  • Get connected to the course server, where Haskell is set up for you.
  • Learn some good Haskell programming practices, including style and testing.
  • Practice writing functions in Haskell — especially recursive functions.
  • Practice writing and using higher-order functions.
  • Use higher-order functions to build complex operations out of simple ones.

Checklist

  • Get connected to the course server
  • Read about good Haskell programming practices
  • Learn about modules and tests in Haskell
  • Get the materials for the assignment (see The Homework Workflow)
  • Implement fib
  • Add tests for intfib
  • Implement intfib
  • Implement step
  • Implement countStep
  • Implement countCallsUntil
  • Implement addition
  • Implement multiplication
  • Implement exponentiation
  • Turn in the assignment (see The Homework Workflow)

Preparing for the Assignment

Before diving into the code, take some time to get set up — this is worth doing properly, and we've built time for it into the assignment. Do your work on the course server, where Haskell and the test libraries are already installed. The How-To Guides cover connecting with VS Code, the homework workflow, and how modules and tests work.

For this assignment, we won't grade for code style — but it's still worth practicing. Use descriptive names and helpful comments where they earn their keep.

Materials and Collaboration

You'll get the starter files from the hw2 template repo: make your own private copy and clone it to the course server. The Homework Workflow walks through every step. Your job is to fill in the definitions marked undefined in Arithmetic.hs and Functions.hs (plus adding the intfib tests described below); the starter code's names and types are fixed, but you can always add helper functions.

Unlike HW1, you can work with a partner on this assignment — remember to follow the pair-programming rules from the syllabus. If you pair up, add your partner as a collaborator on your repo (also covered in that guide).


Functions

In this part, you'll implement several functions emphasizing recursion, ending with a higher-order function. Two things to keep in mind throughout:

  • You're not allowed to change the names or types of anything in the provided starter code — but you're always free to add your own helper functions.
  • Only one task below requires you to write test cases, but you're encouraged to write as many as you like, for all of your code.

fib (10 points)

In Functions.hs, fill in the definition of:

fib :: Integer -> Integer

which computes the Fibonacci number F(n) for n >= 0:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) when n >= 2

(This sequence is often named after Fibonacci, though it was studied by earlier mathematicians too, including Pingala.)

A few notes:

  • The idiomatic way to write this in Haskell is pattern matching, not if/then/else.
  • Go for simplicity over efficiency — an exponential running time is fine here.
  • Run the tests to check your work (the provided suite only covers a handful of cases).

Example

ghci> :l Functions.hs
*Functions> fib 3
Expected output: 2.

intfib (20 points)

It's occasionally useful to consider a more general specification:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) for all integers n

This specification turns out to uniquely determine F(n) for every integer n — not just non-negative ones. So now we can talk about negative Fibonacci terms too.

Before coding, work through a few examples by hand, especially for negative terms. What must F(-1) be? What about F(-2)?

Task 1: In test/FunctionSpec.hs, add at least two test cases for intfib — be sure to test negative numbers. (The test syntax looks a little unusual: copy, paste, and modify one of the existing context blocks. Before implementing intfib, run the suite once to confirm it compiles and that all the new tests fail.)

Task 2: In Functions.hs, implement intfib so it computes F(n) for any integer n. Clarity and correctness matter more than efficiency here — conditionals (if/then/else or guards) are fair game.

step (10 points)

In Functions.hs, fill in:

step :: Integer -> Integer

which computes:

  • f(n) = n / 2, when n is even
  • f(n) = 3n + 1, when n is odd

An if/then/else expression is probably the right tool here. Haskell has built-in even and odd functions, and you'll likely want div for integer division (rather than /, which is fractional).

Example

ghci> :l Functions.hs
*Functions> step 3
Expected output: 10.

*Functions> step 2
Expected output: 1.

countStep (15 points)

In Functions.hs, fill in:

countStep :: Integer -> Integer

Given an Integer n, this returns the fewest number of times step must be called, starting from n, to reach 1.

For example:

  • countStep 1 is 0 — no calls needed, we're already at 1.
  • countStep 2 is 1.
  • countStep 4 is 2.
  • countStep 3 is 7.

Recursion is your friend here.

countCallsUntil (20 points)

Now generalize countStep to count calls to any function of type Integer -> Integer. Define countCallsUntil, which takes three arguments:

  • a function f :: Integer -> Integer
  • a stop value, Integer
  • a start value, Integer

The result is the number of calls to f needed to get from start to stop. For example:

  • countCallsUntil step 1 1 is 0
  • countCallsUntil step 1 3 is 7
  • countCallsUntil (+ 1) 10 0 is 10

Task: In Functions.hs, fill in the definition of countCallsUntil.

Testing Your Functions

Run everything on the course server — it already has GHC and the test libraries. From the assignment directory, run the whole suite:

runhaskell -itest test/Spec.hs

The last line is your summary, e.g. 45 examples, 0 failures. Until you've filled in the stubs you'll see lots of failures and Prelude.undefined — that's expected. To run just one file or one group:

runhaskell -itest test/FunctionSpec.hs
runhaskell -itest test/Spec.hs --match "intfib"

Prefer an interactive session? ghci test/FunctionSpec.hs, then type main at the prompt (:r then main after each edit, :quit when done). See Writing and Running Tests for the full picture.

Passing the provided tests doesn't guarantee your code is fully correct — it's worth thinking up a few test cases of your own.


Higher-Order Arithmetic

Here's the fun part: define addition, multiplication, and exponentiation using only the number 0, an increment operation, and higher-order functions.

The Raw Materials

You're restricted to three building blocks:

zero — just another name for 0:

zero :: Integer
zero = 0

increment — adds one to its argument:

increment :: Integer -> Integer
increment n = n + 1

applyN — a higher-order function taking three arguments: a function f :: Integer -> Integer, a count n :: Integer, and an initialValue :: Integer. It applies f to initialValue, n times. (If n is 0, f isn't applied at all, and the result is just initialValue.) For example:

  • applyN increment 0 1 is 1
  • applyN increment 2 1 is 3

Your Task (50 points)

In Arithmetic.hs, fill in addition, multiplication, and exponentiation. You can assume all three are only ever called with non-negative arguments.

Your implementation is severely constrained: you may use only zero, increment, applyN, and your own addition, multiplication, and exponentiation — nothing else. No numeric literals like 1 or 2, and no built-in + or -.

A few hints:

  • Think before you code — convince yourself an approach will work before writing any Haskell.
  • applyN will show up in each of your three definitions. What function would be a useful argument to it?
  • Build on what you've already implemented — can multiplication be defined in terms of addition?

Example

ghci> :l Arithmetic.hs
*Arithmetic> addition 1 2       -- 3
*Arithmetic> multiplication 2 3 -- 6
*Arithmetic> exponentiation 3 2 -- 9

Testing Your Code

Same as before — run the whole suite with runhaskell -itest test/Spec.hs, or just this part with:

runhaskell -itest test/ArithmeticSpec.hs

(or ghci test/ArithmeticSpec.hs then main, if you'd rather stay interactive).

As before: passing the provided tests is a good sign, but not a guarantee. Think up a few of your own.


Turning It In

Submit on Gradescope, under HW 02: FUNctional Programming:

  1. Run the full test suite one last time: runhaskell -itest test/Spec.hs.
  2. Copy the entire terminal output and paste it into the HW2 text box.
  3. Upload your Arithmetic.hs and Functions.hs to the same assignment (for our records).

The Homework Workflow covers the submission step in context. There's also a short HW 2 reflection to submit separately on Gradescope.

Make sure your final code is committed and pushed to your repo as well.


Where to Go From Here

  • The step function is related to the Collatz conjecture (also known by other names) — whether countStep n terminates for every positive integer n is an unsolved problem in mathematics.
  • Addition, multiplication, and exponentiation are all specific instances of the hyperoperation sequence. Can you define the hyperoperation itself in Haskell, and then define addition, multiplication, and exponentiation as calls to it?