[From Bill Powers (2008.10.11.1140 MDT)]
Tracy B. Harms (2008-10-11 09:40 Pacific) --
So, lets dive into some details. I posted a program,
avg=: +/ % #
and you asked "OK, how is that different from [this]?"
avg := 0.0
for i := 1 to number do avg := avg + input[i];
avg := avg/number;
textout(10,10,inttostr(avg));
The first point of contrast is between what you named "avg" and what I
named "avg". Yours is a scalar variable, mine is a function.
OK. I will rewrite my example as a function instead of a fragment of a program. I assume that a variable called "input" has been declared as an open array of integers with a length determined when numbers are loaded into it, the type having been declared as "inputarraytype." The average is returned as a double-precision floating point number:
function avg(input: InputArrayType): double;
var i, sum: integer;
begin
sum := 0;
for i := 0 to length(input) - 1 do sum := sum + input[i];
result := sum/length(input);
end;
Now if another part of the program is to assign the average of the numbers currently stored in the array called "input" to the value of a variable called "v", it executes the statement
v := avg(input);
Assuming that there is a procedure called "print", we could print the average as
print(v),
or if we didn't need to save the average value to avoid recomputing the same value over and over,
print(avg(input));
The meaning of avg, the function, is ratio-of-total-to-tally, which is
explicit in the notation. The program serves as a statement of the
concept.
Perhaps you can see that once a function is defined in Pascal (by the group of statements that starts with the word "function" above), we can then use the function named "avg" exactly as you use it in the J example (though with a different way of writing the statement).
It looks to me as if defining the function 'avg' is exactly the same in Pascal and J except for the notation. Pascal simply makes all the implicit processes in the J statement explicit. Since the steps done automatically in J are spelled out in Pascal, you can change them in Pascal but not in J.
The meaning of avg, the variable, must be understood by considering
all six places where it occurs. It is used in several ways. Most
prominent are the three assignment statements. Assignments such as
these are not parallel to statements of equality. Plainly avg is not
safely treated as equal to avg + input[i]. Change-over-time is
embedded into the language.
Yes. In Pascal, the colon-equal sign means replacement (wqhich you call assignment), not equality in the logical sense. x := x + 1 means that the current value stored in the memory location named x is replaced by the value of the computation on the right: the old value stored in location x with 1 added to it.
That's quite different from what occurs with copula (=:) in the J
example, where the static nature of things makes it so that avg "is"
what occurs to the right of it. The definition establishes a meaning.
Typical of algebraic expressions, there is no time component here.
I don't know which you are referring to by "here." As far as I can see the only difference between the J and Pascal definitions of the function "avg" is what is spelled out in detail and what is simply understood to happen automatically.
So, of course the you could use "avg" to name a subroutine instead, at
which point the parallel would be somewhat better, but that would not
alter the underlying difference I've just described: The Pascal code
specifies changes over time and involves managing intermediate states
and incidental values. The J code specifies a relationship between
input and output as a composition of functions.
I'm not clear what you mean by that. In Pascal, if we define two functions, f1 and f2, we could certainly write
v := f1(f2(argument list 1), argumentlist 2));
where the arguments of f1 are the value of f2 and whatever further arguments, if any, are required for f1.
You mentioned the basic algebraic relationship, y = f(x) That is a
good map for programs in general (setting aside discussion of
determinacy and stochastic features) so we can think of y as the
output, f as the program, and x as the input data. In the interactive
J console this has the following structure:
f x
y
Parentheses could be used around the x, but they are normally left out
when adjacency has the same meaning.
Also, it looks as if the value of the function is printed out without your asking, when you write just f(x). That's OK, but it's another rule you have to carry in your head.
Now, I'll define 'numbers' to serve as x and the function 'avg' serves as f:
numbers=: 2 9 3 7 5 9 0 4
avg numbers
4.875
The common math notation h(g(f(x))) applies to the following
variation, where we calculate the average where the first and last
elements of the list have been excluded:
avg }.}: numbers
5.5
We could define the Pascal function as
function avg(n1, n2: integer; input: inputarraytype);
where now n1 and n2 are the starting and ending indices of the numbers to be averaged; the "FOR" statement would then be
for i := n1 to n2 do
As far as I can see, there is still no difference other than notation.
You also wondered how you'd calculate the average of every other item
in the list. I'm inclined to define a separate function first, then
apply it:
oddindices=: #~ 2|i.@:#
avg oddindices numbers
7.25
(J uses zero-based indexing, so 9 7 9 4 are the values at the odd
indices here.) This newly defined function was again defined strictly
in terms of other functions. What I've used here is a particularly
"hard core" technique in that it does not involve any specification of
parameters, intermediate vales, or output. You made this comment about
avg:
Similarly, in Pascal we could define an oddindices function, or a whole collection of functions to pick out different patterns of entries in the array, and pass the name of the function to the avg function as an argument, or an index number indicating which of an array of functions is to be used.
> Of course the J program language says all this in one line of symbols, but
> the reader has to imagine the "input" array, initialization of the average,
> the size of the array (or the counting process that checks the size as you
> go), and the indexing process.
Indeed, there is a whole lot for the reader to imagine when it comes
to J. We may even view J as an attempt to minimize specification while
maximizing effect. This does demand a lot of the reader, but it
ultimately lets the reader focus much more exclusively on what
matters. An expression of oddindices in more conventional form would
be:
copy(x, mod(integers(tally(x)),2))
I hope this illuminates how functional programming involves
specification of a program in terms of functions.
What I'm getting out of this so far is that what you called "functional programming" is what I call "programming." Perhaps you might give me an example of programming that isn't functional -- that might make what you mean clearer.
You may be unsatisfied by the fact that my solution to the
odd-numbered-item average did not involve modifying the program avg,
but instead giving it as input only the filtered values. Your program,
in contrast, can be easily modified to increment the index by two each
pass instead of one. That's bound to have a performance advantage,
too.
Or I could do it your way by using a separately-defined function. Nothing in Pascal keeps you from doing that. Generally, if a process is used only once you don't write it as a separate function because calling a function involves a considerable speed penalty -- all the registers have to be pushed into a stack, then restored after the call. That's why strict OOP programs are slow; every reference to a variable through a "method" involves all the overhead of a function call.
There is something else that deserves to be talked about in
contrasting the two averaging programs we've been looking at. While
I'd only demonstrated the use of (+/%#) on a simple list, in fact it
applies at the outermost dimension of matrices of any rank. Here a
simple (2D) example:
avg=: +/ % #
]mtx=: i. 4 6
0 1 2 3 4 5
6 7 8 9 10 11
12 13 14 15 16 17
18 19 20 21 22 23
avg mtx
9 10 11 12 13 14
The result is a list wherein each item is the average of the
corresponding column of the input.
In each case, no modification is made to the J program. The same
simple program handles all of these. The modifications needed to your
Pascal code would be significant. Indeed, once you've handled the
array management required for such flexibility the obvious thing to do
is hide all that code so that it does not get in the way of
understanding the part that is specific to averaging.
Easy to do in Pascal, too, though perhaps more wordy. Input[i,j] refers to the ith column and jth row of a matrix, so a 2-D or n-D matrix of values can be handled quite easily. If you want to leave the dimensions of the matrix unspecified, more labor is required at the level of coding to count the number of entries and adjust the memory allocations at run-time, but once you're written the code to define each such process you don't have to write it again.
All you're talking about here seems to be the degree to which the user of the language leaves the basic underlying operations to the writer of the compiler -- and thereby loses the freedom to invent his own underlying processes. And also has to trust that the other guy did it right.
Such setting-aside-of-mechanics is the most important point I hope to
communicate. Whether it's done by packages (such as Simcon), by making
functions first-class objects, or by making array manipulation
implicit, such techniques help programs reflect design so that the
structured design (the "meaning" of the program) is more prominent.
I don't argue that such techniques aren't useful, expecially to people who don't want to do programming per se but just want a way to generate the results they want. But they require memorizing a lot more arbitrary rules than are needed when you program at a lower level of abstraction, and there's never any guarantee that the person who wrote the lower levels provided the exact operation you want to carry out, or felt any sympathy for your mental limitations.
Rick Marken cheerfully writes simulations in Excel, with variable names like
D7 and Y22 in the formulas, while I, with my leaky memory, lose track immediately of what parts of the physical system they refer to. It wouldn't kill a J programmer if the operation symbols like % were converted to names that give me a chance of working out what they mean, would it? That way I wouldn't have to stop to drag the meanings up out of memory and hope I remembered them right. In fact, I suspect that if you just substituted multicharacter symbols for the single-character ones, and always put in parentheses to remove doubt, and did all the other friendly things that disambiguate and otherwise clarify what is meant, you would end up with program statements that look a lot like Pascal or C, or God forbid, Basic. The actual programming wouldn't look much different.
I'll close by repeating what I said earlier: what you call functional programming looks, so far, like what I call programming -- but without any mnemonic aids at all.
Best,
Bill P.