Originally published in Rebol Forces.

Reb-IT!
Author:  Gregg Irwin
Date: 8-Apr-2002
Comment: Part II (the pair! ripens)
Part: 1 | 2

Contents

Objective

Evaluate the use of the pair! datatype in a non-OO context.

Introduction

In part I of this article, I built a rectangle object that contained various methods in addition to the data for a rectangle. Since writing that article I've gained a little more experience with Rebol and will now present an alternate implementation. Part of the reason for this new approach was a discussion on the Rebol mailing list about the intended use of pair! values. Per the folks at Rebol Technologies, the pair! datatype is there mainly to support View coordinates. The design shown here is more in line with that thinking.

Functions I missed the first time

There are a few functions I didn't know about when I wrote Part I, so I'll list them quickly here, just in case you haven't seen them either.

We'll start with the within? function, because I just can't believe I missed it the first time around.

From the help: within? is used to determine if a point is within a graphical area. You provide the position of the point, the upper left corner of the area, and the area's size. The function will return TRUE if the point is within that area, or FALSE otherwise.

>> within? 50x50 0x0 50x50
== false
>> within? 50x50 0x0 51x51
== true
>> within? 50x50 1x1 50x50
== true

Inside? is a slightly different beast, as it compares only two points against each other. From the help: inside? returns TRUE if both X and Y of the second pair are less than the first.

>> inside? 10x10 10x10
== false
>> inside? 10x10 10x9
== false
>> inside? 10x10 9x9
== true

Reverse is another function that works with pair! values but was omitted from Part I.

>> reverse 1x0
== 0x1
>> reverse -1x-2
== -2x-1

From object to function library

The biggest difference between the object built in Part I, and the new design, is that we no longer maintain the data for the rectangle coordinates. We still use an object! to give the functions a related context, but I would call this type of object a "function server" object as its only purpose is to serve up functions. This works well with Rebol's system of object creation as the object should only need to be instantiated once, so the functions aren't duplicated in memory for every rectangle we may want to deal with.

One advantage to containing the data ourselves was that we knew exactly what format it was in. Now that we'll be getting data passed to us, we need to decide what formats we want to support. Given that pair! values are meant to support View faces, it makes sense to use a design that does the same.

Faces have SIZE and OFFSET words that define their coordinates, so we'll use those. The bonus we get is that our code will work, unchanged, on any object that contains SIZE and OFFSET words! We'll use simple path notation (e.g. rect/size, rect/offset) in our implementation so if you want to put values into simple block, such as [offset 20x20 size 80x80], you can do that as well. Sometimes syntactic sugar is very sweet indeed.

You could add logic to allow passing in a simple block of two pair! values (e.g. [0x0 10x10]) that are treated as a rectangle in exchange for a little additional complexity, but I don't know if it's worth it. By including the SIZE and OFFSET words, you make the data self-describing, which is always a good thing.

A brief digression about function spec templates

I'm going to go off topic here for a moment to discuss an interesting approach to defining functions, which you will see used in the code that follows.

As you may know, a function definition in Rebol is really just 3 blocks of data. A spec block, a vars block, and a body block. Check out the source for the FUNC, DOES, and HAS functions to see how they wrap the underlying FUNCTION function. Now, since Rebol can share blocks very easily, it stands to reason that we should be able to share blocks that represent the various parts of a function definition. In this case, we're going to share some specs that define the interface to a function.

If you have a large library of functions that all have identical, or very similar, interfaces, you can create spec templates for all of them to use. There are tradeoffs, as in everything. One disadvantage is that you don't have the interface right there with the function body for visual inspection as to what parameters it takes and what locals are defined. On the plus side, it can help to enforce consistency and reduce coding quite a bit. I wouldn't consider it a universally applicable idiom. Use it with caution and forethought. If you have a large number of functions that you absolutely need to keep in sync with each other, interface-wise, this could be a very handy technique.

Here are the templates we'll be using:

; Functions that take one rectangle as a parameter
_fn-spec-1: compose [(copy "") r [block! object!] "The rectangle"]
; Functions that take two rectangles as parameters
_fn-spec-2: append copy _fn-spec-1 [r2 [block! object!] "The other rectangle"]
; Functions that take one rectangle, and a pair! value as parameters
_fn-spec-3: append copy _fn-spec-1 [value [pair!]]

The leading underscore is just a naming convention I'm using to denote what should be private values in an object.

As you can see, the templates are just blocks. Take a moment to look at their composed values. You can see that each of them has a leading empty string value. That's intentional. We're going to want to add a function description in that space. We could also do it without the empty string as a placeholder of course. We'll insert the routine description with this little wrapper function; the goal being to make it as easy as possible to define lots of functions with very-similar-but-not-identical specs. In our case, the description is the only thing we're planning to change right now.

mod-fn-spec: func [
{Takes a function spec template and inserts the description in it.}
spec [block!] desc [string!]
][
head change copy spec desc
]

I said earlier that Rebol was good at sharing blocks, and it is. You probably noticed, though, that I'm copying the template block in mod-fn-spec, as well as when building fn-spec-2 and fn-spec-3 from fn-spec-1 so, technically, they aren't shared. We're reusing the definitions to build new specs, but those new specs will need to be independent to support the cases where we modify them, as we do to add the routine descriptions.

And now, back to our regularly scheduled program

We'll start with a couple little functions to help us create objects that comply with the type of rectangle objects we're expecting to deal with.

; make a rectangle object
_rect: make object! [offset: size: 0x0]
 
_make-rect: func [offset [pair!] size [pair!]] [
make _rect compose [offset: (offset) size: (size)]
]

Now, we'll implement some of the most basic functions. Notice that these functions take the _fn-spec-1 template as their spec block. The commented version of TOP? shows what it would look like without using the template.

;top?:    func [r [block! object!] "The rectangle"]
top?: func _fn-spec-1 [r/offset/y]
left?: func _fn-spec-1 [r/offset/x]
height?: func _fn-spec-1 [r/size/y]
width?: func _fn-spec-1 [r/size/x]
right?: func _fn-spec-1 [add left? r width? r]
bottom?: func _fn-spec-1 [add top? r height? r]
; upper-left and lower-right shortcuts
ul?: func _fn-spec-1 [r/offset]
lr?: func _fn-spec-1 [add r/offset r/size]

Now we'll do a quick before-and-after comparison for each of our function spec templates, to see if they're worth the trouble.

Before: (without templates)

empty?: func [
"An empty rectangle is one with no area"
r [block! object!] "The rectangle"
][
any [(0 >= height? r) (0 >= width? r)]
]
 
equal?: func [
"Equal rectangles have the same coordinates"
r [block! object!] "The rectangle"
r2 [block! object!] "The other rectangle"
][
all [(r/offset = r2/offset) (r/size = r2/size)]
]
 
grow: func [
{Grows a rectangle by the specified amounts. Negative values shrink it.}
r [block! object!] "The rectangle"
value [pair!]
][
r/size: add r/size value
r
]

After: (with templates)

empty?: func mod-fn-spec _fn-spec-1
"An empty rectangle is one with no area"
[
any [(0 >= height? r) (0 >= width? r)]
]
 
equal?: func mod-fn-spec _fn-spec-2
"Equal rectangles have the same coordinates"
[
all [(r/offset = r2/offset) (r/size = r2/size)]
]
 
grow: func mod-fn-spec _fn-spec-3
{Grows a rectangle by the specified amounts. Negative values shrink it.}
[
r/size: add r/size value
r
]

I have to admit that I really like seeing my parameter list right there with the function definition, but there are definitely some advantages to the template approach as well. Again, I wouldn't consider it a universally applicable technique but, rather, a specialized one. Good template names would be a big help as well. Even with only one and two parameters in our function specs, we get a decent reduction in typing effort. If you had a larger number of parameters, with descriptions and multiple data types listed, the gain could be substantial.

Pros and cons, learning as we go

When I wrote part I of this article, I was a rank beginner with Rebol. I'm a little less rank now, but not much. Rebol is so malleable that you can make it become what you want it to be. This two part article doesn't begin to scratch the surface of what that really means. We could easily have used another language for this example, but Rebol's flexibility goes far, far beyond the question: "To OOP, or not to OOP?"

I can tell you that I greatly prefer this second implementation, mainly because it seems to "fit" better with the design of Rebol itself and how pair! values are used with View faces. There is at least one place where this model isn't quite seamless however: effect/draw commands. Draw commands, for boxes and such, want only the coordinates of the box, not an object or block of values. To deal with this, you'll probably want to use COMPOSE. There's a small bit of sample code at the end of the article that shows how this might be accomplished, and how some of the rectangle functions can operate directly on faces.

Another design issue to consider is whether to update rectangle parameters directly in the case of functions like INFLATE/DEFLATE and GROW/SHRINK. Currently they are modified, but I'm not sure yet if the savings are worth the side effects. Maybe just a couple more helper functions...

The complete code

Here is the complete code for a rectangle function library object.

rect: make object! [
; make a rectangle object
_rect: make object! [offset: size: 0x0]
 
_make-rect: func [offset [pair!] size [pair!]] [
make _rect compose [offset: (offset) size: (size)]
]
 
; These are function interface templates.
; One rectangle as a parameter
_fn-spec-1: compose [(copy "") r [block! object!] "The rectangle"]
; Two rectangles as parameters
_fn-spec-2: append copy _fn-spec-1 [r2 [block! object!] "The other rectangle"]
; One rectangle, and a pair! value as parameters
_fn-spec-3: append copy _fn-spec-1 [value [pair!]]
 
mod-fn-spec: func [
{Takes a function spec template and inserts the description in it.}
spec [block!] desc [string!]
][
head change copy spec desc
]
 
 
;top?: func [r [block! object!] "The rectangle"]
top?: func _fn-spec-1 [r/offset/y]
left?: func _fn-spec-1 [r/offset/x]
height?: func _fn-spec-1 [r/size/y]
width?: func _fn-spec-1 [r/size/x]
right?: func _fn-spec-1 [add left? r width? r]
bottom?: func _fn-spec-1 [add top? r height? r]
; upper-left and lower-right shortcuts
ul?: func _fn-spec-1 [r/offset]
lr?: func _fn-spec-1 [add r/offset r/size]
 
empty?: func mod-fn-spec _fn-spec-1
"An empty rectangle is one with no area"
[
any [(0 >= height? r) (0 >= width? r)]
]
 
equal?: func mod-fn-spec _fn-spec-2
"Equal rectangles have the same coordinates"
[
all [(r/offset = r2/offset) (r/size = r2/size)]
]
 
 
inflate: func mod-fn-spec _fn-spec-3
{Inflates a rectangle by the specified amounts. The change occurs
in all directions. I.e. the offset will change as well as the size.
Negative values deflate the rectangle.}
[
r/offset: subtract r/offset value
r/size: add r/size (value * 2)
r
]
 
deflate: func mod-fn-spec _fn-spec-3
{Deflates a rectangle by the specified amounts. The change occurs
in all directions. I.e. the offset will change as well as the size.
Negative values inflate the rectangle.}
[
inflate r negate value
]
 
grow: func mod-fn-spec _fn-spec-3
{Grows a rectangle by the specified amounts. Negative values shrink it.}
[
r/size: add r/size value
]
 
shrink: func mod-fn-spec _fn-spec-3
{Shrinks a rectangle by the specified amounts. Negative values grow it.}
[
grow r negate value
r
]
 
move: func mod-fn-spec _fn-spec-3
{Moves a rectangle by the specified amounts. Negative values are allowed.}
[
r/offset: add r/offset value
r
]
 
 
contains?: func mod-fn-spec _fn-spec-3
{Returns true if the specified point lies within the rectangle.}
[
within? value r/offset r/size
]
 
intersects?: func mod-fn-spec _fn-spec-2
"Returns true if the rectangles intersect."
[
to-logic any [
(contains? r ul? r2)
(contains? r lr? r2)
(contains? r2 ul? r)
(contains? r2 lr? r)
all [
(left? r) < (right? r2)
(left? r2) < (right? r)
(top? r) < (bottom? r2)
(top? r2) < (bottom? r)
]
]
]
 
intersection: func mod-fn-spec _fn-spec-2
{Returns the intersection of the two rectangles as an object
containing offset and size values.}
[
either intersects? r r2 [
_make-rect
maximum ul? r2 ul? r
; Have to subtract the intersection offset to get the size
subtract minimum lr? r2 lr? r (maximum ul? r2 ul? r)
][
_make-rect 0x0 0x0
]
]
 
union: func mod-fn-spec _fn-spec-2
{Returns the union of the two rectangles as an object
containing offset and size values.}
[
_make-rect
minimum ul? r2 ul? r
; Have to subtract the union offset to get the size
subtract maximum lr? r2 lr? r (minimum ul? r2 ul? r)
]
 
]

And a little test code to see how some of it works.

rect-lib-test: func [face-1 face-2 draw-face /local r] [
draw-face/effect: compose/deep [
draw [
; Show where the faces are now
pen blue
box (rect/ul? face-1) (rect/lr? face-1)
pen orange
box (rect/ul? face-2) (rect/lr? face-2)
; Resize the faces. This is a bit odd, being inside
; the draw commands we're building, but it works.
(rect/inflate face-1 5x5)
(rect/deflate face-2 5x5)
; Back to draw commands
pen black
box (rect/ul? r: rect/intersection face-1 face-2) (rect/lr? r)
pen red
box (rect/ul? r: rect/union face-1 face-2) (rect/lr? r)
]
]
show [face-1 face-2 draw-face]
]
 
lay: layout [
b1: box yellow 100x100
at 50x50
b2: box green 100x100
at 175x20
button "Test" [rect-lib-test b1 b2 b3]
button "Quit" [quit]
; This is our drawing surface
at 0x0
b3: box 175x175 effect [draw copy []]
]
 
view lay