Project: the way out
Sweep the bay, decide with a pipeline of pure functions built by composition, map, filter and fold, then drive out.
Do this lesson in the simulatorBugBot is parked in a bay, walled in on three sides. It has to look around, work out which way is open, and drive out, stopping a safe distance short of whatever is beyond. You could write that as one loop full of variables. In this project you write it the functional way: the robot's side effects (sensing, turning, driving) stay in a thin layer at the edges, and every decision in between is made by pure functions built from partial application, composition, map, filter and fold.
The brief
- Sweep (side effects): take 8 distance readings, one every 45 degrees, starting facing the way the robot starts and turning right 45 degrees after each reading, so the robot ends facing its start direction. Return them as a tuple of
(heading, cm)pairs, whereheadingis 0, 45, 90 and so on up to 315.- Decide (pure): work out how much room each direction has, which is the reading minus a safety margin of 15 cm. Print the headings with more than 25 cm of room, and fold the rooms to find the direction with the most.
- Act (side effects): turn right to that heading and drive forward by its room.
Decompose it
the way out
├── edge: sweep() 8 readings -> ((0, cm), (45, cm), ...)
├── core: pure functions
│ ├── offset, compose small curried pieces
│ ├── room_of a reading -> its room, built by composition
│ ├── with_room (heading, cm) -> (heading, room), mapped over the sweep
│ ├── open filter: room > 25
│ └── roomier fold: keep the pair with more room
└── edge: act turn right to the best heading, drive its room
Everything in the core can be tested with made-up readings, without a robot. Only sweep and the last two commands touch the world.
Step 1: the sweep
The sweep is the one place readings come in. It returns an immutable tuple, so nothing later can change the data it collected. It is a side-effecting function by nature, so a loop here is fine; the functional style is about keeping such code small and separate.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def sweep():
readings = ()
for i in range(8):
readings = readings + ((i * 45, distance()),) # a new tuple each time
turn_right(30, angle=45)
return readings
print(sweep())
On the task's mat, facing into the bay, the readings are about:
| Heading | 0 | 45 | 90 | 135 | 180 | 225 | 270 | 315 |
|---|---|---|---|---|---|---|---|---|
| cm | 26 | 34 | 61 | 34 | 26 | 34 | 30 | 34 |
| room (cm − 15) | 11 | 19 | 46 | 19 | 11 | 19 | 15 | 19 |
So the answer should be heading 90, with about 46 cm of room, and it is the only direction with more than 25. Work this out before you code, so you know what the program should print.
Step 2: build the pieces
Test each pure function on its own with values you choose:
def offset(amount):
return lambda x: x + amount
def compose(g, f):
return lambda x: g(f(x))
def cm_of(reading):
return reading[1]
room_of = compose(offset(-15), cm_of) # cm_of first, then subtract the margin
print(room_of((90, 61.0))) # 46.0
print(room_of((0, 26.0))) # 11.0
room_of is never written with def; it is the composition of two smaller functions, one of them made by partial application. Its type is (heading, cm) → real.
Step 3: map, filter, fold
With made-up readings, still no robot:
from functools import reduce
def offset(amount):
return lambda x: x + amount
def compose(g, f):
return lambda x: g(f(x))
room_of = compose(offset(-15), lambda r: r[1])
test = ((0, 26.0), (90, 61.0), (180, 40.0))
rooms = tuple(map(lambda r: (r[0], room_of(r)), test))
wide = tuple(filter(lambda r: r[1] > 20, rooms))
best = reduce(lambda a, b: a if a[1] >= b[1] else b, rooms)
print(rooms)
print(wide)
print(best)
This gives ((0, 11.0), (90, 46.0), (180, 25.0)), then ((90, 46.0), (180, 25.0)), then (90, 46.0). When the fold compares two equal rooms, >= keeps the earlier one, so a tie always goes to the direction swept first.
Step 4: act
The robot finishes the sweep facing heading 0, so turning right by the best heading faces it that way. Turning right 90 then driving 46 cm takes it out of the bay. Guard against a sweep that finds no room at all: do not drive a negative distance.
Test plan
| Test | What to check | Expected |
|---|---|---|
| 1 | room_of((90, 61.0)) |
46.0 |
| 2 | the fold on ((0, 5.0), (45, 5.0)) |
(0, 5.0): a tie keeps the first |
| 3 | the printed open headings | open: 90 |
| 4 | the printed best | best: 90 with <about 46> cm of room |
| 5 | the robot | ends facing about 90, outside the bay, having touched nothing |
| 6 | the code | map, filter and reduce used; no max, min or sorted |
Task: the way out
Get BugBot out of the bay, following the brief. Every part of the program is defined here:
MARGIN = 15(cm) andOPEN_CM = 25(cm) are constants.sweep()takes no arguments and returns a tuple of 8(heading, cm)pairs, heading0, 45, ..., 315and cm thedistance()reading taken facing that way, turning right 45 degrees at speed 30 after each reading.offset(amount)returns a function of one numberxthat givesx + amount;compose(g, f)returns a function of one valuexthat givesg(f(x)).room_ofiscomposeapplied tooffset(-MARGIN)and a function that returns a pair's cm: given a(heading, cm)pair it returns the room in cm.- Map over the sweep to get
(heading, room)pairs. Filter them to those with room greater thanOPEN_CMand print their headings in sweep order, separated by spaces, asopen: <headings>. - Fold the
(heading, room)pairs to the one with the most room (the earlier one on a tie) and printbest: <heading> with <room> cm of room, with the room rounded to one decimal place. - Turn right by the best heading at speed 30, and if the room is more than 0, drive forward by the room at speed 50.
Use map, filter and reduce; do not use max, min or sorted. The robot must not touch anything.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
from functools import reduce
MARGIN = 15
OPEN_CM = 25
def sweep():
readings = ()
return readings
readings = sweep()
print("open:")
Challenges
- Replace the fold with a recursive function
roomiest(pairs)that uses only head, tail and a test for the empty tuple. - The sweep is the only impure part. Write a pure function
plan(readings)that returns(heading, room)for the move to make, so the whole decision can be tested in one call without a robot. - Change
with_roomso it keeps the reading too,(heading, cm, room). Which other functions have to change, and which do not? What does that tell you about the types of the functions?