2011-09-20

SICP Exercise 2.3: Rectangles

Implement a representation for rectangles in a plane. (Hint: You may want to make use of exercise 2.2.) In terms of your constructors and selectors, create procedures that compute the perimeter and the area of a given rectangle. Now implement a different representation for rectangles. Can you design your system with suitable abstraction barriers, so that the same perimeter and area procedures will work using either representation?

To make this easy on ourselves, let's assume that the sides of our rectangles run parallel to the axes. Then we can easily produce a couple of representations for rectangles:
  1. Have two points to represent diagonally opposite corners of the rectangle.
  2. Have a point to represent a single corner and the width and height for the rectangle.
We defined a constructor and selectors for points in exercise 2.2, so we can use those here.

Let's start with the first representation. We want our constructor to take two points, and we can put these in a pair:
(define (make-rect p1 p2)
  (cons p1 p2))
What selectors shall we have? Well, let's try to cover all bases. We'll have selectors for the four corner points, one to get the width and one to get the height. Note that to select any particular corner point we can take the two points the rectangle was constructed with, extract their x and y components and create a new point using either the smaller or larger of the two x values and either the smaller or larger of the two y values. Whether we select the smaller or larger for each value depends upon which corner it is:

Cornerx Valuey Value
Bottom LeftSmallerSmaller
Bottom RightLargerSmaller
Top LeftSmallerLarger
Top RightLargerLarger

Scheme provides a couple of procedures as standard, min and max, which we can use to select the appropriate values in each case. For example, we could define our procedure to get the bottom-right point as:
(define (get-bottom-right r)
  (let ((p1 (car r))
        (p2 (cdr r)))
    (make-point (min (x-point p1) (x-point p2))
                (max (y-point p1) (y-point p2)))))
However, we can take this a step further. All of the corner selectors will follow this same pattern, so we can extract out a higher-order procedure that takes in the rectangle and the two procedures that select which x and y values to use. Then each of our corner selectors can just invoke this appropriately:
(define (get-point r xselector yselector)
  (let ((p1 (car r))
        (p2 (cdr r)))
    (make-point (xselector (x-point p1) (x-point p2))
                (yselector (y-point p1) (y-point p2)))))

(define (get-bottom-left r)
  (get-point r min min))

(define (get-bottom-right r)
  (get-point r max min))

(define (get-top-left r)
  (get-point r min max))

(define (get-top-right r)
  (get-point r max max))
Finally we need the width and height selectors. These can just find the absolute difference between the x or y values of the two points respectively:
(define (get-width r)
  (abs (- (x-point (car r)) (x-point (cdr r)))))

(define (get-height r)
  (abs (- (y-point (car r)) (y-point (cdr r)))))
We could, of course, have extracted another higher-order procedure here that takes the rectangle and the value selector (i.e. x-point or y-point) and performs the calculation. However in this case, given that the calculation is a simple single-line expression and there are only two occurrences of it, I'm happy to leave them as is.

Let's see this in action:
> (define r1 (make-rect (make-point 1 3) (make-point 6 5)))
> (define r2 (make-rect (make-point 7 5) (make-point 3 4)))
> (get-bottom-left r1)
(1 . 3)
> (get-bottom-right r1)
(6 . 3)
> (get-top-left r1)
(1 . 5)
> (get-top-right r1)
(6 . 5)
> (get-width r1)
5
> (get-height r1)
2
> (get-bottom-left r2)
(3 . 4)
> (get-bottom-right r2)
(7 . 4)
> (get-top-left r2)
(3 . 5)
> (get-top-right r2)
(7 . 5)
> (get-width r2)
4
> (get-height r2)
1
Now that we've defined our constructor and selectors we can move on to defining the procedures for calculating the perimeter and area of a rectangle. Given a rectangle of width w and height h we know we can calculate the perimeter as 2(w+h) and the area as wh. Handily, we have selectors for the width and height of a rectangle, so we can translate these formulae pretty directly into procedures:
(define (perimeter r)
  (* 2 (+ (get-width r) (get-height r))))

(define (area r)
  (* (get-width r) (get-height r)))
And let's put these to the test:
> (perimeter r1)
14
> (perimeter r2)
10
> (area r1)
10
> (area r2)
4
Okay, so that all seems to work, so let's move on to the second representation. Now if we allow the width and/or height to be negative then the corner point can be any of the points of the rectangle. For example, if we have a positive width and negative height then the point will represent the top-left corner. Let's see if we can put this into practice:
(define (make-rect p w h)
  (cons p (cons w h)))

(define (get-point r xselector yselector)
  (let ((p (car r)))
    (make-point (xselector (x-point p) (+ (x-point p) (cadr r)))
                (yselector (y-point p) (+ (y-point p) (cddr r))))))

(define (get-bottom-left r)
  (get-point r min min))

(define (get-bottom-right r)
  (get-point r max min))

(define (get-top-left r)
  (get-point r min max))

(define (get-top-right r)
  (get-point r max max))

(define (get-width r)
  (abs (cadr r)))

(define (get-height r)
  (abs (cddr r)))
And here's the second representation in action...
> (define r1 (make-rect (make-point 1 3) 5 2))
> (define r2 (make-rect (make-point 7 5) -4 -1))
> (get-bottom-left r1)
(1 . 3)
> (get-bottom-right r1)
(6 . 3)
> (get-top-left r1)
(1 . 5)
> (get-top-right r1)
(6 . 5)
> (get-width r1)
5
> (get-height r1)
2
> (get-bottom-left r2)
(3 . 4)
> (get-bottom-right r2)
(7 . 4)
> (get-top-left r2)
(3 . 5)
> (get-top-right r2)
(7 . 5)
> (get-width r2)
4
> (get-height r2)
1
> (perimeter r1)
14
> (perimeter r2)
10
> (area r1)
10
> (area r2)
4

3 comments:

  1. Everything about this exercise is confusing.

    First of all, the task is unclear.
    "In terms of your constructors and selectors, create procedures that compute the perimeter and the area of a given rectangle."
    Why would the rectangle constructor compute the perimeter or area?

    It seems like this was interpreted as:
    "Create constructors and selectors which can be used to create procedures that compute the perimeter and the area" which would make more sense.

    But then,
    "let's assume that the sides of our rectangles run parallel to the axes"
    Why??
    Wouldn't the simplest rectangle implementation be:

    (define (make-rectangle s1 s2) (cons s1 s2))
    (define (length r) (car r))
    (define (width r) (cdr r))

    I've looked at some other solutions and they all make this same assumption, but so far, no other exercise in the book seems to require such assumptions... why would this one?

    ReplyDelete
    Replies
    1. >> "Why would the rectangle constructor compute the perimeter or area?"

      The task isn't asking you to create a constructor that computes the perimeter or area. It states that there must be procedures that can calculate perimeter or area, not that the constructor must do this. "Create constructors and selectors which can be used to create procedures that compute the perimeter and the area" is the correct interpretation. You'll note that neither of my implementations calculates perimeter or area in the constructor.

      As to why you would do this, well there are some situations where repeated calculations have a significant performance impact and so it's desirable to precache the results of the calculations so that you only perform them once.

      >> "let's assume that the sides of our rectangles run parallel to the axes"
      >> Why??
      >> Wouldn't the simplest rectangle implementation be: [...]

      Because I'm lazy :-)

      The exercise is getting you to define a representation of a rectangle in a 2-dimensional plane. In order to correctly represent such a rectangle you need to know not only the dimensions of the rectangle, but also where it lies relative to that plane. It's not enough to know it's width and height, but also where in the plane it is positioned and how it is rotated relative to the axes of the plane.

      By assuming that the sizes of the rectangles run parallel to the axes I can eliminate the rotation portion of the representation... And eliminate a whole bunch of extra calculation. If you want to have some fun and explore such representations you could use e.g. {corner point, angle of rotation, side length #1, side length #2}, {corner point, adjacent corner point, length of perpendicular side}, {corner point, opposite corner point, angle of rotation for side from first corner point}.

      Delete
    2. >> "The task isn't asking you to create a constructor that computes the perimeter or area."

      Agreed that one would guess so. I was just pointing out that the phrasing is slightly confusing.

      >> "The exercise is getting you to define a representation of a rectangle in a 2-dimensional plane. In order to correctly represent such a rectangle you need to know not only the dimensions of the rectangle, but also where it lies relative to that plane."

      The exercise task is hinting at making use of exercise 2.2, so we already have a representation of points and segments, which does hold the positional information. My point was, why not just use two segments to represent the rectangle?

      Delete