Lau B. Jensen wrote:
> Clojure lets you be really functional with all
> the goodness that comes with Lisp, allowing you
> to produce source code which is extraordinary
> concise and expressive.
>
> J however, takes brevity to an extreme which
> for many things is great, but for flow control
> etc, looks awful.
This sounds like an apologia; you praise Clojure for being terser than
other languages, but upon encountering a language terser yet, you condemn
it (or, conversely, defend Clojure). Do believe Clojure has found "the
perfect amount of brevity"?
Anyway, I wasn't saying Lisp was verbose, I was saying it was too verbose
to be effective for interactive (REPL) development. I'm sure it's fine to
work with in a file editor, with the occasional flip to the REPL to
interrogate some data or play with a function.
But to be clear: the majority of my J development takes place directly in
the REPL. I only flip to the file editor to persist code I've perfected
in the REPL.
> Thats my oppinion, yours might differ.
It does. Lack of explicit, imperative flow control, and letting flow fall
naturally out of expressions, is another part of J's elegant appeal.
Let's take an example:
Pseudocode:
if ( pathString.lastChar != "\" )
{
pathString.append("\");
}
vs J:
(, '\' #~ '\'~:{:) pathString
Which, in broad strokes, reads "append to the input N copies of '\', where
N is 0 or 1 according to whether the last char of the input is already a
'\'". Which is more elegant? The hamhanded "exceptional case handler" in
the pseudocode, or the J that treats all citizens equally, doesn't branch,
and uses the output of the test as the actual value that calculates the
value to append? In J, the test and the response are inseparable. Truly
functional.
Or how about this:
if ( checkInput(input) )
{
print "OK";
}
else
{
print "BAD";
}
vs J:
(;:'BAD OK') {::~ checkInput
which reads "select the Nth message from the possible responses according
to the result of checkInput". What if you wanted to add a third possible
response, and altered checkInput accordingly? You'd have to add an elseif
to the pseudocode; all the J would need is the new message added to the
literal; that's it (not to mention in a strongly-typed language checkInput
would probably need its declaration altered to output an int rather than a
bool).
Or, have you seen the power operator?
int processList(List someList)
{
// coerce list to table
return processTable( new Table(someList) );
}
int processTable(Table someTable)
{
// table processing code
}
vs J:
coerce =: ,:^:(1...@$)
processTable =: tableproces...@coerce
where tableProcessor takes normalized, table input, and coerce reads "apply
the function ,: (rank promotion) N times, where N is 1 or 0 according to
whether the input is a list or not". Of course, coerce is so concise it
wouldn't normally be broken out into a separate function, but I did that
for pedagogical purposes (in the pseudocode, you're pretty much forced to
have another lengthy function, with redundant decoration, or to muddy the
implementation of processTable with extraneous parameter-checking code,
which isn't part of its core function).
We can even take this one step further:
coerece =: ,:^:(2...@$)
with minimal changes (2 characters), I now coerece *any input* into a
table: lists still work, but now so do scalars, tables (left untouched),
and even a higher-ranked arrays! The code now reads "apply the function
,: (rank promotion) N times, where N is two minus the input's rank". And
what is 2 minus the inputs rank for the various cases:
list: rank 1, 2-1=1, so promote a list once
scalar: rank 0, 2-0=2, so promote a scalar twice (once to a
list, and a second time to a table)
table: rank 2, 2-2=0, so promote a table 0 times (i.e. don't
apply the function at all)
cube : rank 3, 2-3=-1, so ...
wait, what? How can you apply a function -1 times? Easy, you apply it's
*inverse* once. J has a whole calculus of functions, and can calculate
function inverses. And, of course, the inverse of rank promotion is rank
demotion, so:
cube : rank 3, 2-3=-1, so demote a cube once
tesseract: rank 4, 2-4=-2, so demote a tesseract twice (once
to a cube, and a second time to a
table)
See how "flow control" isn't special syntax of the language. It isn't even
a seperate concept in a J program. It's just a flowing part of the
design. So I'm not sure why you think "terse flow control looks awful" [1].
But maybe you were thinking of another kind of flow control? In my
experience, if-statements and for-loops are the most common flow control
statements in a program written in a "normal" (von Neumanish) language.
We just covered if-statements, and as you know, for-loops are implict in J
(and so unneccesary).
But what about some others?
while =: ^:break_clause^:_:
Here's an adverb you can apply to any code (which would equivalent of the
loop body) to create a while loop. We've met ^: before, it's the power
conjunction. Here we see it twice, in ^:break_clause and ^:_: .
Let's discuss the latter.
That _: is J's notation for infinity. So, read literally, ^:_: is
"apply the function an infinite number of times" or "keep reapplying
forever". This is related to a while-loop's function, but it's not very
useful if applied literally. So ^:_: and its kin were defined to mean
"apply a function to its limit", that is, "keep applying the function
until its output matches its input". In that case, applying the function
again would have no effect, because the next iteration would have the same
input as the previous (remember J is a functional language). So there's
no point in applying the function even once more: it has reached its
limit. For example:
decimate =: %&10 NB. Divide by 10
decimate^:_: 10000 NB. Decimate until there's nothing left
Here, we keep dividing by 10 until there's nothing left (this wouldn't ever
stop in mathematics, but will necessarily happen eventually with
floating-point arithmetic). We can visualize this by showing the
intermediate steps:
decimate^:a: 10000
10000 1000 100 10 1 0.1 0.01 0.001 0.0001 1e_5 1e_6 ...
So ^:_: applies a function to its limit. OK, what about
^:break_condition ? We've met that phrase before, in ,:^:(2...@$) .
Again, it's the same concept: apply the function on the left as specified
by the output of the function on the right. In the case of _: the
output is "infinity", in the case of break_condition the output will be
0 or 1 depending on the input (a break condition is boolean).
So if the input is "right" (i.e. processing is done), then the
break_condition will be 0, whence loop_body^:break_condition^:_: will
become loop_body^:0^:_: . Obviously, loop_body^:0 applies the
loop_body zero times, which has no effect. To "have no effect" is to
leave the input untouched; put another way, it copies the input to the
output ... but if the input matches the output, then the function has
reached its limit! Obviously ^:_: detects this fact and terminates.
Voila, a while loop!
Note, there was no separate, special-case "while" built into the language
from the beginning, nor was there any need of one. We just apply J's
functional calculus, as consistent applications of the notation. So I
still don't know why you think our "flow control looks awful".
Maybe you're thinking of select/case? We've got that too. We spell it @.
and it's functional. For example:
foo`bar`baz`buz`[email protected]
In the simplest case, the select function is applied to the input, and
outputs an integer which determines which of foo, bar, baz, ... will be
applied to the input. In more complex cases, one could select multiple
functions to apply simultaneously, or even to apply an arbitrary
composition the functions. J can manipulate and apply arrays of verbs
(functions) as easily as arrays of nouns.
If you're still convinced, I'll point out that J programs have a lot less
"flow control" than you'd find in other languages, as a result of its
abstraction from loops (and the boundary conditions they guard against),
its suite of high-level primitives that already handle most common "corner
cases" under the covers, and its consistent design which means lots of
other edge conditions fall out "naturally". So you won't have to worry
about our "ugly flow control" very often.
For example, because i. returns #+1 for unfound items, then you can
write split=: ({.;}.)~ i.&fret and if your fret exists, the items before
it will be in the head and the items that follow in the tail, and if it
doesn't then the head will be full and the tail empty. You'd also be
surprised how often 1=*/'' helps you out, either directly or indirectly
(the phrase means that the product of an empty list is 1, because 1 is the
identity element of *, and of course many primitives do multiplication
under the covers, so this fact helps out a lot when you're passed an empty
list as input; you needn't check for or guard against it explicitly).
And if these pleasures not thee move, then here is a consolation prize: J
provides "normal" flow control structures [2], e.g.
if. foo do. bar else. baz end.
and
while. this do. that end.
But these structures are usually the last refuge of the desperate J
programmer (especially for the loopy ones).
> Clojure here, is different than Scheme, CommonLisp et al
> Clojure:
> (defn func [arg]
> (let [x 5]
> x))
> Common Lisp:
> (defn func (arg)
> (let (x 5)
> x))
Uh, didn't you just change the shape of (some of) the parens? That doesn't
reduce the visual intrusion (for me, anyway). But I believe you when you
say a Lisp programmer stops seeing them. I mean, I can read %&10^:a:
10000 with ease (and pleasure). I don't think many non-Jers would
believe me if I told them that, and most would complain that J's spelling
scheme is a wart in the language, when it's actually one of its most
elegant aspects.
-Dan
[1] Before you say it, yes, I'm sure you can emulate all this in Lisp, or
Java, or any other language. But the point is this is native and natural
in J, and it promotes and reflects a different kind of thinking (programs
as algebra).
[2] http://www.jsoftware.com/help/primer/control_structure.htm
----------------------------------------------------------------------
For information about J forums see http://www.jsoftware.com/forums.htm