This work has been submitted to Morgan Kaufmann Publishers for
possible publication. Copyright may be transferred without notice,
after which this version may no longer be accessible.
----------------------------------------------------------------
This file is copyright 2004 Mark Jason Dominus. Unauthorized
distribution in any medium is absolutely forbidden.
----------------------------------------------------------------
H I G H E R O R D E R P E R L
TUTORIAL INTRODUCTION
'Linogram' is a system for specifying and drawing diagrams. It
is explained in detail in chapter 9 of _Higher-Order Perl_, by
Mark Jason Dominus. See 'http://hop.perl.plover.com/' for
complete details.
A 'linogram' program is a file, or a series of files, that
define the shapes that are to be drawn and their relationships
to one another. Each object to be drawn is called a _feature_
and is an instance of a _type_. The type has the definition of
what the feature looks like and how it behaves.
Initially, 'linogram' knows only one type, called 'number',
which is simply a number. Other types are built from this. For
example, a point has an `x' and a `y' coordinate, so its
definition is simply
define point {
number x, y;
}
A line is determined by two points, called the 'start' And the
'end', so it could be defined like this:
define line {
point start, end;
}
But in fact the definition in 'linogram''s standard library is a
little more interesting:
define line {
point start, end, center;
constraints {
center = (start + end)/2;
}
}
This says that a line has three named points, and that the
center point is halfway between the start and the end points.
'Linogram' can use this definition to compute the center if it
knows the start or the end point, or it can compute the start if
all it knows is the center and the end. Any two of the points
determine the third.
How do we include a line in a diagram? The input file that you
give to 'linogram' defines a type, called the "root type". The
definition of the root type is just like any other type, except
that you omit the "define ... {" at the beginning. So you can
say
line base, side1, side2;
and your diagram will contain three lines. You can constrain the
positions of the lines and get a triangle:
side1.start = side2.start;
base.start = side1.end;
base.end = side2.end;
Since 'side1', 'side2', and 'base' are lines, each has a start,
and end, and a center point already defined. The center of the
base is a point called 'base.center'. And since points are
defined as having x and y coordinates, we can refer to the
coordinates of the center of the base as 'base.center.x' and
'base.center.y'.
We could force the center of the base to be located in a certain
place by putting in a constraint:
base.center.x = 3;
base.center.y = 4;
which we may abbreviate to
base.center = (3, 4);
The '(3, 4)' is called a _tuple_. 'linogram' uses tuples to
represent locations and displacements.
We can constrain the base of the triangle to be horizontal:
base.start.y = base.end.y;
We can also constrain the triangle to be isosceles by requiring
the intersection of 'side1' and 'side2' to be located directly
above the midpoint of the base:
side1.start.x = base.center.x;
We don't have to mention 'side2' here because 'linogram' already
knows that `side2.start.x' = `side1.start.x'. We could put that
constraint in, if we wanted, but 'linogram' would recognize that
it was redundant, and ignore it.
Similarly, once we've said that
base.start.y = base.end.y;
we don't have to explicitly require that
base.start.y = base.center.y;
'linogram' knows from the definition of 'line' that
center.y = (start.y + end.y)/2;
for all lines, including 'base', and so if `start.y' is the same
as `end.y', it can figure out that 'center.y' is the same also.
If we're going to have a lot of triangles of this sort, it makes
sense to wrap all this up in our own type definition:
define triangle {
line base, side1, side2;
constraints {
side1.start = side2.start;
base.start = side1.end;
base.end = side2.end;
base.start.y = base.end.y;
side1.start.x = base.center.x;
}
}
Now we can say
triangle T1, T2;
to make two triangles, each with its own 'side1', 'side2', and
'base'. Each triangle contains three sides, each of which has
two points, each of which has two coordinates, so the triangle
is ultimately defined by 12 numbers:
The x and y coordinates of the apex
The x and y coordinates of the lower left vertex
The x and y coordinates of the lower right vertex
The x and y coordinates of the middle of the left side
The x and y coordinates of the middle of the right side
The x and y coordinates of the middle of the base
But not all the numbers are required, because the constraints
relate the numbers to each other. In fact, only 4 numbers are
required to define the location of one of these triangles: you
need the `y' coordinate of the apex, the two `x' coordinates of
the two base vertices, and the single shared `y' coordinate of
the two base vertices. From these, 'linogram' will figure
everything else out.
T1.base.start = (5,3)
T1.base.end = (9,3)
T1.side1.start = (7,5)
Here we specified six numbers, but two of them are redundant.
Once 'linogram' knows that `T1.base.start.y' is 3, it will
figure out that `T1.base.end.y' is also 3 without our having to
say so; if the y-coordinates in the first two lines didn't
match, 'linogram' would complain that we were violating a
constraint. And we said explicitly that `T1.side1.start.x' is 7,
but 'linogram' can figure that out too. It knows that
T1.base.center.x = (T1.base.start.x + T1.base.start.y)/2 = (5 +
9)/2 = 7, and it also knows that T1.side1.start.x =
T1.base.center.x, so is also 7. Again, it doesn't matter that we
gave 'linogram' this redundant information, but it will check it
for us.
Having to say 'T1.side1.start' to locate the apex is a little
annoying. We can easily add an alias to the triangle:
define triangle {
line base, side1, side2;
* point apex;
constraints {
* apex = side1.start = side2.start;
base.start = side1.end;
base.end = side2.end;
base.start.y = base.end.y;
* apex.x = base.center.x;
}
}
Now instead of saying
T1.side1.start = (7,1)
we can say
T1.apex = (7,1)
and since 'apex' is defined to be equal to 'side1.start', these
are equivalent.
We can also add aliases for the length of the base and the
altitude of the triangle if we think that will be convenient:
define triangle {
line base, side1, side2;
* number wd, ht;
point apex;
constraints {
apex = side1.start = side2.start;
base.start = side1.end;
base.end = side2.end;
base.start.y = base.end.y;
* ht = apex.y - base.center.y;
* wd = base.end.x - base.start.x;
apex.x = base.center.x;
}
}
and now we can define a particular triangle:
triangle T1;
constraints {
T1.base.center = (7, 3);
T1.wd = 4;
T1.ht = 2;
}
and we have a triangle whose base is at (7, 3) with a width of 4
and a height of 2; 'linogram' figures out where everything else
goes.
One final abbreviation might be convenient. We can define a
reference point in the triangle and use it to specify the
location of the triangle itself instead of the location of the
center of the triangle's base. We might put the reference point
in the middle of the triangle, or at the apex, or in the middle
of the base, or wherever seems convenient:
define triangle {
line base, side1, side2;
number wd, ht;
* point loc;
* number x, y;
point apex;
constraints {
apex = side1.start = side2.start;
base.start = side1.end;
base.end = side2.end;
base.start.y = base.end.y;
ht = apex.y - base.center.y;
wd = base.end.x - base.start.x;
apex.x = base.center.x;
* (x, y) = loc = base.center;
}
}
Here I've put the reference point in the middle of the base. Now
instead of locating 'T1.base.center' explicitly, we can just
locate 'T1.loc', and we can also ask about 'T1.x' and 'T1.y' to
get the coordinates of this reference point.
There's also a shorthand notation for this:
triangle T1;
constraints {
T1.loc = (7, 3);
T1.wd = 4;
T1.ht = 2;
}
If you prefer, you can write it like this:
triangle T1(wd=4, ht=2, loc=(7,3));
which might look nicer.
Now maybe we want two triangles connected by an arrow:
triangle T1(wd=4, ht=2, loc=(7,3)), T2;
arrow a(start = T1.side2.center, end = T2.side1.center);
constraints {
T2 = T1 + (3, 0);
}
arrows are just like lines---they have a start and an end
point---except they're drawn with arrowheads at the end point.
Here we've required that the arrow `a' goes from the right side
of `T1' to the left side of `T2'. And `T1' is clearly located.
But where is `T2'? And how big is it?
The constraints say that triangle `T2' is exactly the same size
and shape as `T1', but three units due east. If we had written
T2 = T1 + (3, 1)
it would have been three units east and one unit north. If we
change the 0 to a 1 in this way, we don't have to worry about
the fact that the arrow, which used to be horizontal, is now
diagonal. The arrow is constrained to go from one triangle to
the other, and 'linogram' will figure out what it must look like
in order for that to be true, and draw it properly.
Suppose we're going to draw a diagram that talks about process
management, and each triangle represents a process. We'll have
lot of these four-by-two triangles. It would get annoying to ask
for a lot of triangles that all have 'wd=4' and 'ht=2'. We
should make a new type with a logical name:
define process extends triangle {
point start, end;
constraints { wd = 4; ht = 2;
start = side1.center; end = side2.center; }
}
This defines a new type that has all the same numbers, lines,
and points that a triangle has, and all the same constraints,
and some additional ones as well. In particular, `wd' and `ht'
have been constrained to be 4 and 2, so every "process" is a
triangle of a specific size.
Now instead of this:
triangle T1(wd=4, ht=2, loc=(7,3)), T2;
arrow a(start = T1.side2.center, end = T2.side1.center);
constraints {
T2 = T1 + (3, 0);
}
we can write this:
process P1(loc=(7,3)), P2;
arrow a(start = P1.end, end = P1.start);
constraints {
P2 = P1 + (3, 0);
}
Or we could have a whole string of processes:
number xspc = 0.75;
process P1(loc=(0,0)),
P2 = P1 + (xspc, 0),
P3 = P2 + (xspc, 0),
P4 = P3 + (xspc * 1.5, 0),
P5 = P4 + (xspc, xspc * -0.25);
arrow a12(start=P1.end, end=P2.start),
a23(start=P2.end, end=P3.start),
a34(start=P3.end, end=P4.start),
a45(start=P4.end, end=P5.start);
If an assembly like this is going to be common, we could define
a new type to represent it; the type would include five
processes and four arrows. A later version of 'linogram' will
make it easier to define types that contain long sequences of
objects like this one does.
Suppose now that you have drawn a large, complex diagram that
involves a large number of 'process' objects, and that 'process'
was defined like this:
require "box";
define process extends box { }
That is, you've defined your 'process' object to be nothing more
than a box. Suppose that you suddenly decide that you want to
see what the diagram will look like if all the process boxes are
triangles instead. No problem, it's easy. Just replace the
definition of 'process':
define process {
triangle T;
point n = T.apex,
s = base.center,
e = side2.center,
w = side1.center,
nw = (n+w)/2,
sw = base.start,
ne = (n+e)/2,
se = base.end,
c = (n + s) / 2;
number wd = T.wd, ht = T.wd;
hline top, bottom;
vline left, right;
constraints {
}
draw { T; }
}
LANGUAGE REFERENCE
* '*'_foo_ means zero or more instances of _foo_
* '+'_foo_ means one or more instances of _foo_
* '?'_foo_ means zero or one instances of _foo_
* '' means that "foo" must appear literally
program = *declaration perl_code
declaration = require | definition | feature
| draw_section | constraint_section
require = string <;>
definition = name ?( name) <{> *declaration <}>
feature = ? type declarator *(<,> declarator) <;>
declarator = name
?( <(> param-spec *(<,> param-spec) <)> )
?( <=> expression )
param-spec = cname <=> expression
draw_section = <{> *drawable <}>
drawable = <&> name | cname
type = C | C | name
cname = name | cname <.> name
constraint_section = <{> *constraint <}>
constraint = expression +(<=> expression)
expression = term + expression | term - expression
term = atom * expression | atom / expression
atom = funcall
| name
| tuple
| number
| <(> expression <)>
funcall = name <(> expression <)>
tuple = <(> expression <,> expression *(<,> expression) <)>
perl_code = <__END__\n> (any perl code)
LANGUAGE REFERENCE
The basic 'linogram' object is called a _feature_. A feature
contains some instructions about how it should be drawn, a list
of subfeatures that it contains, their names, and a list of
constraints on the subfeatures.
A 'linogram' file is a essentially a definition of a single
feature called the "root feature". The subfeatures of the root
feature are the boxes and arrows that make it up. You tell
'linogram' what boxes and arrows are included in the root
feature, and how they are related to one another, just as you
would tel it about any other type, such as the triangles in the
tutorial.
There are just a few things that can appear in a definition:
* 'require' declarations
A declaration like
require "foo";
tells 'linogram' to pause what it is doing, locate 'foo.lino'
somewhere, and read it in. Then it picks up where it left
off. Often, 'foo' will be a standard 'linogram' library file.
For example, 'linogram' has a standard definition of a 'box'
that you can load in by saying
require "box";
after which you may declare features to have type 'box':
box a, b;
But you can also require a file that contains definitions
that you wrote yourself.
* Constraint declarations
A constraint declaration looks like this:
constraint {
equations...
equations...
equations...
}
An equation is a pair of arithmetic expressions, separated
with an equal sign, such as:
T1.start.x = 12;
or:
start + end = 2 * center;
As a convenience, an equation may have more than one equal
sign; something like this:
a = b = c = d;
is interpreted as shorthand for this:
a = b;
a = c;
a = d;
The important thing to know is that equations _must_ be
linear. No multiplication or division may involve more than
one feature.
* type definitions
* constraint and drawable declarations
* "require" declarations
* perl code
Type definitions are just more collections of
RUNNING 'LINOGRAM'
STANDARD FEATURE DEFINITIONS
* 1. POINTS
* 2. LINES
line, hline, vline, arrow
* 3. LABELS
A label is a location (that is, a point) with which a string is
associated. The 'label' object has the following parameters:
* 'text' (mandatory): the text that is displayed
* 'font': Defaults to 'times'
* 'size': Defaults to 10
* 4. BOXES
** 4.1. BOX
A box is a rectangle, with its edges oriented parallel to the
axes. The edges are named 'top', 'bottom', 'left', and 'right'.
'top' and 'bottom' are 'hline's; 'left' and 'right' are 'vline's
The four vertices of the box are 'point's named 'nw', 'ne',
'sw', and 'se'. The midpoints of the four sides are 'n', 's',
'w', and 'e'. The center of the box is a point named 'c'.
The height and width of the box are 'ht' and 'wd', respectively.
** 4.2. LABELBOX
A 'labelbox' is the same as a 'box', except that it has a
'label' in the middle. The 'label''s 'text' parameter is
exported as 'text', but the others, such as 'font', must be
accessed as 'label.font', etc.
** 4.3. ELLIPSE
An 'ellipse' is similar to a box, except that it's elliptical.
The 'ht' and 'wd' variables are the lengths of the major and
minor axes of the ellipse, which are always vertical and
horizontal.
The 'ellipse' has 'n', 'nw', etc., but no 'top', 'bottom',
'left', or 'right'.
** 4.4. OVAL
An 'oval' is like a 'box', except that the corners may be
rounded off. The radius of curvature of the rounded corners is
'rad'.
STANDARD DRAWING LIBRARIES
* 1. POSTSCRIPT OUTPUT
* 2. BITMAP OUTPUT
* 3. ASCII-ART OUTPUT
IMPLEMENTING DRAWING LIBRARIES
----------------------------------------------------------------
define TYPE {
subpart name, name, name...;
subpart name(v=EXPR, ...);
constraints {
EXPR = EXPR = EXPR ... ;
}
draw { &perl_function;
subpart; subpart;
}
}
require "file";