Progress Exam 1 — Pyret

First half of class. Open book, open notes, no AI. Pyret, not Java.

Monday, September 21, the first half of class. Fifty minutes, four problems. The exam has been sat: the four problems and reference solutions are at the end of this page.

How it works

Four problems, four answer boxes, one to a page. You write the code in code.pyret.org, the same as any other day.

  1. Copy the grey box at the top of each question into Pyret. It is already a comment, so it runs as it stands.
  2. Write your answer underneath it.
  3. Paste that answer back into the box on the page before you move on.

There is no countdown on the screen. The finish time is on the board, and everything is in by 2:55. You can go back to an earlier problem whenever you want, and each box keeps whatever you last put in it.

You do not need to sign in to Pyret, and you do not need an import line — map, filter, fold, length, member, reverse, sort and range are all there as they are.

You get a test drive the Wednesday before. The first half of the class on September 16 is a practice exam: same length, real problems, no marks. That is the single most useful hour you will get, and it only works if you turn up having already practiced.

Open book and open notes. The textbook, this website, your own files from the last four weeks, anything you have written down. Pyret’s own error messages and documentation too.

No AI and no collaboration. The course AI policy applies in full, and on an exam it is not negotiable. You work alone.

What it covers

Everything we have done in Pyret before the practice test on Wednesday, September 16, which is three weeks:

Style is graded, not just correctness

The syllabus says exams are “graded for both correctness and style”, and this is the first time that has mattered to you. Concretely, on this exam:

  • Every function has a contract and a doc: line.
  • Every function has a where: block, with examples you worked out yourself rather than read off your own body.
  • Names say what the thing is. u is fine for a unit because that is what the course has called it all term; x for a list of colors is not.

A correct function with no examples does not get full marks. A function with good examples and a body you could not finish gets most of them. That is the same deal A3 offered and it is deliberate.

Decide now what you will do if you run short. Go through all four problems and write the contract, the doc: line and the where: examples — pasting each one in — before you write any bodies. The deal above holds on every problem, and a problem you never open is the one that gets nothing.

Practice, and how to prepare

Rewrite one problem from each assignment, from a blank file, without looking at your old answer. That is the whole of it. Re-reading your A2 is a much weaker exercise than writing it again, because recognizing a solution and producing one are different skills and only the second one is on the exam.

Practice Exam 1 is that same exercise with the problems already picked: four of them, with worked answers underneath.

The postmortems — A1 and A2 — say what the room actually got wrong.

How Pyret runs your program and how to design functions are the two pages worth having open during the exam.

If something goes wrong

Save your work the way you do every class — getting your work off the lab machine applies during an exam too.

Make-ups are covered by the syllabus: one per exam, voluntary, requested within a week of the grade going up. If you cannot be there on September 21, tell me before the day rather than after it.

The four problems

As they appeared, one to a page, each in a grey box to copy into Pyret.

Problem 1 — a picture

Write buoy. It takes one number, u, and gives back a picture: an orange circle of radius u / 2, centered on a white square of side u * 2.

overlay(front, back)          # puts the first picture on top of the second
circle(radius, mode, color)
square(side, mode, color)

Every measurement in the body comes from u, so that buoy(10) and buoy(40) are the same picture at two sizes.

Two examples is enough on this one.

Problem 2 — one question, with or

Write admission. It takes an age and whether the person lives in town, and gives back the price in dollars:

  • someone who lives in town pays 3, whatever their age
  • otherwise, someone under 5, or 70 and over, pays 6
  • otherwise, 11

Call the second parameter is-resident.

Write it as one if / else if / else. Write the middle case as one question using or, not as an if inside an if.

Four examples at least. Two of them are at exactly 5 and exactly 70, which is where a wrong < or >= shows up. One of them is a resident who is also under 5 — that is the only example that catches the three cases being asked in the wrong order.

Problem 3 — filter, then map

Write overtime. It takes a list of hours worked and gives back, for each entry that is over 8, how many hours over 8 it is. Exactly 8 is not overtime.

overtime([list: 6, 8, 10, 13])  is  [list: 2, 5]

Use filter and map together. Two passes, not one.

Three examples. One is a list where nothing survives the filter, and one contains exactly 8.

Problem 4 — by hand, with cases

Write total-meters. It takes a list of lap counts and gives back the total distance swum. One lap is 25 meters.

No library function that walks the list for you — not filter, not map, not fold, not length. Use cases.

Three examples, and the first one is the empty list. Write that one, and its answer, before you write any of the body.

Reference solutions

Every example here runs under use context starter2024, which is what code.pyret.org runs.

1. buoy

fun buoy(u :: Number) -> Image:
  doc: "an orange circle of radius u / 2 centered on a white square of side u * 2"
  overlay(circle(u / 2, "solid", "orange"), square(u * 2, "solid", "white"))
where:
  buoy(4) is overlay(circle(2, "solid", "orange"), square(8, "solid", "white"))
  buoy(10) is overlay(circle(5, "solid", "orange"), square(20, "solid", "white"))
end

Any construction that draws the same picture is right; Pyret compares pictures by appearance, so rectangle(u * 2, u * 2, ...) in place of square passes. What does not pass is a measurement that does not come from u: a literal 8 where u * 2 belongs draws buoy(4) and not buoy(10).

2. admission

fun admission(age :: Number, is-resident :: Boolean) -> Number:
  doc: "the price in dollars: residents 3, under 5 or 70 and over 6, everyone else 11"
  if is-resident:
    3
  else if (age < 5) or (age >= 70):
    6
  else:
    11
  end
where:
  admission(30, true) is 3
  admission(3, true) is 3
  admission(3, false) is 6
  admission(5, false) is 11
  admission(70, false) is 6
  admission(69, false) is 11
end

is-resident has to be asked first. Ask the age bands first and a four-year-old resident pays 6 — and every other example above still passes. admission(3, true) is 3 is the one that catches it, which is why the problem asked for it by name.

3. overtime

fun overtime(hours :: List<Number>) -> List<Number>:
  doc: "for each entry over 8, how many hours over 8 it is"
  map(lam(h): h - 8 end, filter(lam(h): h > 8 end, hours))
where:
  overtime([list: ]) is [list: ]
  overtime([list: 6, 8, 10, 13]) is [list: 2, 5]
  overtime([list: 3, 7]) is [list: ]
end

Filter first, so that exactly 8 never reaches the subtraction. Subtracting first and then keeping what is over 0 is also correct; it is the “two passes” wording it falls short of, not the arithmetic.

4. total-meters

fun total-meters(laps :: List<Number>) -> Number:
  doc: "the total distance swum, where one lap is 25 meters"
  cases (List) laps:
    | empty => 0
    | link(f, r) => (f * 25) + total-meters(r)
  end
where:
  total-meters([list: ]) is 0
  total-meters([list: 4]) is 100
  total-meters([list: 4, 10]) is 350
  total-meters([list: 1, 2, 3]) is 150
end

| empty => 0 is the line the problem asked you to write first. A wrong empty branch fails every example, not only the empty one: with empty => 1, total-meters([list: 4]) comes out at 101.