Hi, Jeff,
Thanks for chipping in!
Most of my musings and comments arise from a well-meaning attempt to
find a balance between Sir Arthur Eddington
There are two kinds of science, Physics and stamp collecting.
and Ralph Waldo Emerson
A foolish consistency is the hobgoblin of little minds...
Of course we can't omit Lily Tomlin
Man invented language to satisfy his deep need to complain.
from our list of philosophers! ;-)
I certainly hope that my remarks are coming across as (part of)
my attempt to help give "aid and comfort" to REBOL. That's the
spirit in which I intend them. My lists were not meant as
value judgements of any kind (much less, of a negative kind),
but as a suggestion of factors that affect every language,
every programmer, and every question, all in varying degrees.
I've been on the list for about a year now, and occasionally see
questions or comments go by followed by a response that tends
toward (I'm exaggerating for effect here!), "Well, you just don't
'get' REBOL yet. You're still thinking in some other language."
As I tried to say in my earlier post, a change of mind-set is
often worth including in the "pain pill", but I think it is also
sometimes too easy to assume that the entire problem is ONLY due
to "switching pain". (I'm not picking on Holger here; he just
happened to use a memorable phrase that caught my fancy!) By
listing a large number of possible factors, I'm trying to
encourage a multimodal approach to pain therapy.
I freely confess that as a sometime teacher, I'm constantly
searching for better, simpler, clearer ways to explain the
concepts of software design and development, and the tools of
that discipline -- including programming languages. I'm also
thoroughly persuaded by Dijkstra's statements that
* complexity is the chief enemy of software developers, and
* elegance is not a luxury, but a necessity.
[EMAIL PROTECTED] wrote:
>
> Yeah, but when we respond then you guys don't let us off the
> hook and you hold us to the fire!! :-) Hah hah -- Just jokin
> around, ya-know!
>
Well, it's more fun that pulling legs off of ants! ;-)
OBTW, please note that I kept saying "the language" in an attempt
at a constant reminder that I wasn't criticizing REBOL. I was
offering a list that I think can apply to almost ANY language.
>
> > 3) The language does not have sufficient documentation to
> > allow the programmer to learn the language -- without
> > exhaustive trial-and- error experimentation.
>
> We've lost Carl for weeks on end while he did a rewrite of
> our docs for reason 3 above.
>
And I, for one, am VERY grateful. It's a big step forward. I also
am working up a list of errata and suggestions for improvement (but
in background mode, due to schedule pressures). I'll send them as
soon as I can.
However, one significant area I can think of (at least the current
pebble in my shoe ;-) is the need for documentation on contexts.
Being a Bear of Small Brain, it's taken me quite a while to get my
head around a few facts on that subject (and with quite a bit of help
from the list, I must say!). However, I still can't find anywhere
in the available documentation where I can go to confirm or refute
what I think I've learned on that subject.
>
> > 4) The language has features whose complexity requires that
> > the programmer keep up with more "moving parts" and
> > exceptions to be able to use them safely.
>
> Few things in REBOL can be generally classed number 4 above
> -- or at least fewer things than many (most) other languages
> out there. Simple things are simple to do, and you can do
> hard things too (but hard things can still be hard!).
>
A few examples, to make the point:
a) We can convert integer! and decimal! values to time! values,
with the (reasonable, IMHO) assumption that the units are
seconds.
>> to-time 3600 == 1:00
>> to-time 360.5 == 0:06:00.5
However, we can't go the other way (which I would think of
as equally reasonable).
>> to-integer to-time 3600
** Script Error: Invalid argument: 1:00.
** Where: to integer! :value
>> to-decimal to-time 360.25
** Script Error: Invalid argument: 0:06:00.25.
** Where: to decimal! :value
b) Most loop control functions (e.g., Forskip, Foreach, For,
Repeat) "localize" the controlled word(s). However, Forall
does not do so, leaving the value of the controlled word
altered upon loop exit.
c) On the subject of "simple things are simple to do" I'd ask:
Isn't changing a value in a data structure as simple a concept
as retrieving it? Isn't changing (or retrieving) a value based
on a calculated "place" in the structure almost as simple a
concept as changing (or retrieving) a value at a fixed "place"
(except for the effort of calculating that "place")? Given an
array (nested blocks of equal size)...
>> a: array/initial [3 3 3] 0
== [[[0 0 0] [0 0 0] [0 0 0]] [[0 0 0] [0 0 0] [0 0 0]]
[[0 0 0] [0 0 0] [0 0 0]]]
...it's easy to alter and fetch values at fixed positions...
>> a/1/1/1: 111 a/2/2/2: 222 a/3/3/3: 333 a/2/2/2
== 222
>> a
== [[[111 0 0] [0 0 0] [0 0 0]] [[0 0 0] [0 222 0] [0 0
0]] [[0 0 0] [0 0 0] [0 0 333]]]
...and not very hard to fetch values at easy-to-compute
positions...
>> for i 1 3 1 [print a/:i/:i/:i]
111
222
333
...but now write a simple expression to set those "diagonal"
positions back to zero! (Without explicitly writing three
set-path expressions, please... That's not a general
solution!) It's not nearly so easy as
for i 1 3 1 [a/:i/:i/:i: 0]
...because that's not legal code! One can certainly create
such monstrousities as...
for i 1 3 1 [
do reduce [
to-set-path reduce ['a i i i]
0
]
]
== [0 0 0]
>> a
== [[[0 0 0] [0 0 0] [0 0 0]] [[0 0 0] [0 0 0] [0 0 0]]
[[0 0 0] [0 0 0] [0 0 0]]]
...but that's hardly keeping "simple things simple"! There
are other variations on this same theme, such as incrementing
all values in the array, incrementing an element selected by
three values obtained from input, etc. but they all share the
same theme of significantly-different effort to do things that
are conceptually equally easy.
d) On the subject of confusing inconsistencies vs. "foolish
consistency" we can consider modifications to series values.
>> s-b: ["a" "b" "c" "d"] == ["a" "b" "c" "d"]
>> s-l: make list! s-b == make list! ["a" "b" "c" "d"]
>> s-h: make hash! s-b == make hash! ["a" "b" "c" "d"]
>> s-s: "abcd" == "abcd"
When we Append to any of these, the side effect is to extend the
series and the result is the extended series. Reasonable and
consistent, IMHO.
>> append s-b "e" == ["a" "b" "c" "d" "e"]
>> append s-l "e" == make list! ["a" "b" "c" "d" "e"]
>> append s-h "e" == make hash! ["a" "b" "c" "d" "e"]
>> append s-s "e" == "abcde"
When we Change any of these, the side effect is to alter a position
in the series, but the result is equivalent to Next on the altered
series. Consistent between series types, but inconsistent with
the result of Append. I think this is a perfectly reasonable
inconsistency, as it allows a sequence of "chained" applications
of Change to affect consecutive positions, rather than repeatedly
altering the same position (which probably wouldn't make sense!)
In other words, there is a "higher consistency", in that the result
of each operation caters to successive applications of the same
operation in a natural way.
>> change s-b "x" == ["b" "c" "d" "e"]
>> change s-l "x" == make list! ["b" "c" "d" "e"]
>> change s-h "x" == make hash! ["b" "c" "d" "e"]
>> change s-s "x" == "bcde"
However, the reference to the series does not have its position
altered; again this seems perfectly reasonable.
>> s-b == ["x" "b" "c" "d" "e"]
>> s-l == make list! ["x" "b" "c" "d" "e"]
>> s-h == make hash! ["x" "b" "c" "d" "e"]
>> s-s == "xbcde"
When we Insert a value, the side effect is to modify the series,
and the result is positioned immediatly following the insertion.
Again, this follows our "higher consistency" idea of supporting
consecutive Insertions in left-to-right order.
>> insert s-b "z" == ["x" "b" "c" "d" "e"]
>> insert s-l "z" == make list! ["x" "b" "c" "d" "e"]
>> insert s-h "z" == make hash! ["x" "b" "c" "d" "e"]
>> insert s-s "z" == "xbcde"
BUT, this time there is a datatype-dependent inconsistency!
Most of the series references have unaltered positions, but
for the list! case, the position is different.
>> s-b == ["z" "x" "b" "c" "d" "e"]
>> s-l == make list! ["x" "b" "c" "d" "e"]
>> s-h == make hash! ["z" "x" "b" "c" "d" "e"]
>> s-s == "zxbcde"
This means that I have an inconsistent side-effect, with no
offsetting benefit that I can think of. If I want to write
a generic series-handling function which uses Insert, I may
have to make a special-case test for whether the argument
series is a list! and include
>> s-l: head s-l == make list! ["z" "x" "b" "c" "d" "e"]
code to re-establish a consistent state of affairs. The
same issue recurrs (with a different side-effect on list!
values) with Remove.
Inconsistency is not always unreasonable, but there should
be a good reason!
e) The RCUG specifically states that "the concept of none is not
the same as an empty block, empty string, or null character".
Yet there's an inconsistency in Parse when an empty string
(zero IS a valid string length) matches a pattern.
split-string: function [
s [string!] d [string!]
][
blk seg delim data
][
data: complement delim: charset d
blk: copy []
parse/all s [
copy seg any data (append blk seg) any [
delim copy seg any data (append blk seg)
]
]
blk
]
>> split-string "this~is~a~test" "~"
== ["this" "is" "a" "test"]
>> split-string "this~is~a~~test~with~~empty~segments" "~"
== ["this" "is" "a" none "test" "with" none "empty"
"segments"]
Parse evaluated the zero-length segments in the last case as
being NONE instead of being "". The cost of this inconsistency
is that one must either complicate the function above into
split-string: function [
s [string!] d [string!]
][
blk seg delim data
][
data: complement delim: charset d
blk: copy []
parse/all s [
copy seg any data (append blk any [seg ""]) any [
delim copy seg any data (append blk any [seg ""])
]
]
blk
]
>> split-string "this~is~a~~test~with~~empty~segments" "~"
== ["this" "is" "a" "" "test" "with" "" "empty"
"segments"]
or remember to post-process the result to replace NONE with
empty strings, or (shudder!) write all down-stream code to
accept NONE as equivalent to an empty string.
This has the downstream cost of biting anyone who uses Parse-XML
results, expecting to be able to use Select to fetch attribute
values, interpreting a result of NONE as indicating that the
attribute did not appear.
>> foo: parse-xml {<a b="1" c="">Hi!</a>}
== [document none [["a" ["b" "1" "c" none] ["Hi!"]]]]
At some point in code that traverses the nested block structure
we'd arrive at the equivalent of...
>> attribs: second first third foo
== ["b" "1" "c" none]
...and then say something like...
>> select attribs "c"
== none
...which will require additional code to distinguish from...
>> select attribs "x"
== none
...perhaps something like...
if found? find attribs "c" [any [select attribs "c" ""]]
== ""
if found? find attribs "b" [any [select attribs "b" ""]]
== "1"
if found? find attribs "x" [any [select attribs "x" ""]]
== none
We could have avoided all of this if Parse treated empty strings
as empty strings.
There is certainly some interaction between this point and point #6,
but I'll deal with that question at that "point". ;-)
>
> > 5) The language has features that are "emergent properties"
> > rather than "designed concepts" -- that is, they are
> > consequences of the interactions of other features of the
> > language which even the language designer(s) may not have
> > intended or anticipated.
>
[snip]
> ...REBOL is full of these things, where you suddenly realize a
> new use for an old function.
>
That's not at all what I meant by an emergent property. It's not
an issue of whether I as a programmer think of an interesting way
to use the value of an expression, whether it's a trick that works
in other languages
> foo: any [val1 val2 val3 default-val]
or one that's more specific to REBOL features
> to-tuple loop 3 [append [] random 255]
> if loop 20 [do-something now/time > 12:00:00] [print #afternoon]
Rather, its a question of whether the language's concepts or
implementation interact in a way that imposes limits on how I can
do things or causes subtle, context-dependent changes in behavior
that may be difficult to anticipate.
For example, Use provides a way to provide a small "scope" for
temporarily-needed words without worrying about name collisions
with the surrounding context. (This is A Good Idea, as REBOL
has situations in which the clearest way to write code is to
set a variable to the value of an expression, and then use the
variable, rather than using the expression directly.)
However, Use is apparently implemented via definitional scope,
rather than dynamic scope or lexical scope. This means that
utilizing Use inside a recursive function may have exactly the
opposite effect from its intended purpose.
f: func [a /local b] [
either a = 0 [
exit
][
use [c] [
c: a
b: a
print ["in " c b]
f (a - 1)
print ["out" c b]
]
]
exit
]
>> f 3
in 3 3
in 2 2
in 1 1
out 1 1
out 1 2
out 1 3
Here, instead of shielding the inner block from the outer world,
Use actually exposes that block to interference from elsewhere.
Surely I'm not to believe that we have a mechanism that was designed
specifically to be unusable with recursive functions? Isn't it
reasonable of me to assume that this is an unintended emergent
consequence of the separate choices to allow functions to be recursive
and to implement Use via definitional scoping?
The most straightforward way I know of to get the desired degree
of "insulation" is to write
f: func [a /local b] [
either a = 0 [
exit
][
do func [/local c] [
c: a
b: a
print ["in " c b]
f (a - 1)
print ["out" c b]
]
]
exit
]
>> f 3
in 3 3
in 2 2
in 1 1
out 1 1
out 2 2
out 3 3
and that't not a terrible cost (for an old Lisp hacker, at least),
but I suspect it's not terribly obvious to the typical beginning
REBOL programmer.
One more example, and I'll close this point.
Does is nice syntactical sugar for a no-argument, no-locals function
that packs up some behavior in a single word.
Another nice feature of REBOL is the fact that words "controlled" by
(most!) looping functions are localized, so that after the loop exit
the prior value of the word has not been destroyed by the loop.
However, this apparently was implemented using a context/scoping model
rather than a save/restore model.
The consequence of these two facts is that I cannot use a previously-
defined Does block which has access to the loop-controlled variable(s).
obscurity: function [
n [integer!]
][
mystery reveal
][
mystery: -1 reveal: does [print mystery]
reveal
for mystery 1 4 1 [reveal]
reveal
]
>> obscurity 4 ; instead of the possibly-expected
-1 ; -1
-1 ; 1
-1 ; 2
-1 ; 3
-1 ; 4
-1 ; -1
If I want to use a single definition (either for space or maintenance
purposes) of a block to be evaluated at various places -- including
within a loop -- I have to write it as a function with a parameter
(so that I can pass the loop control value in to it).
>
> > 6) The implementation of the language may be faulty --
> > compared either to its published specifications or (absent
> > such specs) the initial intentions of its designer(s).
>
> By any other name: BUG. Yes, software has bugs. Bugs are
> frustrating.
>
Yes, but it's more frustrating to be uncertain about whether
something IS a bug! I've seen disagreement on this list between
experienced REBOL programmers over whether a particular piece of
behavior is a bug or not. Each had a rationale for why there
was or was not a bug, based on a mental model of REBOL. From my
perspective, the question was undecidable, as neither could show
a specification that clearly demonstrated which mental model was
correct.
The point I deferred from #4 above was that almost ANY kind of
unpleasantness or surprise about a language's behavior can be
attributed to failure of the programmer's mental model,
questionable design, faulty implementation, or poor documentation.
If someone wants to label any of my examples from #4 as a bug
instead of an inconsistency in design/implementation, that's
OK (especially if it gets it fixed faster ;-), but that should
not change the fact that built-in complexity is still a different
issue than an actual defect.
>
> > 7) The language "grew" rather than being "designed".
>
> The language's design grew...
>
And I'm glad that it did! That only proves that it is a "living
language" instead of an archaeological artifact. Again, I'm not
being negative here, just pointing out that as a language grows
in areas or ways that weren't part of the original vision, little
gotchas can creep in and begin to accumulate.
I think you agreed with me on this point, in your following
paragraph.
>
> The areas where REBOL has "grown" is in the "features"
> layer. Features are sometimes added which don't support the
> full gamut of operations that most other datatypes have, or
> they have some exception to a rule. Features occasionally
> break the consistency that the design of REBOL has sought
> (in my opinion very successfully) to achieve otherwise.
>
In the following, though, I must disagree a bit.
>
> We all want to understand what hurdles people face in
> learning and adopting REBOL. We rap about this often in the
> office, and your breakdown above is pretty good, though I
> would throw out 7, 5 and 4 and just look at the remaining
> issues: Bugs and docs.
>
The end of that sentence is where we part company. I'm not
willing to discard any avenue of inquiry into how we programmers
come to be confused about a language and what might be done
either to prevent or alleviate that confusion. I'm also not
persuaded that REBOL (nor any programming language designed by
mortals ;-) is so perfect in design and concept that any issues
must be considered as implementation glitches, explanation
breakdowns, or user head gaps.
E.g., the documentation says that, in evaluating paths in blocks,
that "an implicit select is performed".
>> gpa: ["a" 4 "b" 3 "c" 2 "d" 1 "f" 0]
== ["a" 4 "b" 3 "c" 2 "d" 1 "f" 0]
>> letter: "a" == "a"
>> select gpa letter == 4
>> gpa/:letter == 4
Seems to be the same, until somebody creates a new grade without
telling the programmers...
>> letter: "i" == "i"
>> select gpa letter == none
>> gpa/:letter
** Script Error: Invalid path value: i.
** Where: gpa/:letter
Is this really a bug? Is this an inconsistency that's there
for some historical reason? Is there some benefit to having
these two be subtly different? I think the questions are worth
asking, rather than ruling some possibilities out of bounds.
>
> To those two I'd add support.
>
You're absolutely right! I'm glad you added it!
> ... They want tutorials, and FAQs and how-tos,
> and cool examples, etc. REBOL Tech.'s worked hard on
> providing a lot of that stuff, and many others have as well,
> but this is one big area where any one can contribute in a
> meaningful way to the spread and adoption of REBOL.
>
I hope these essays are serving such an end.
>
> (As Dan recently mentioned, we're also working on a
> contribution system where people can contribute to the
> language more directly... More details to come
> soon!!)
>
How exciting!! I'm really glad to hear more!!
>
[discussion of my typos snipped, as I responded to that elsewhere]
>
> Here is a case of a subtlety about infix operators that may
> occasionally bite people (one for the docs).
It's not just infix operators; it's the larger problem of identifying
what constitutes an "expression" in REBOL. There are few syntactic
structures, so layout and semantics become crucial. (Witness the
difficulty of writing a pretty-printer for REBOL source code that
truly gives an intuitive layout!)
Also, to steal an illustration from ALGOL 60 days...
YOU CAN'T SAY THAT
AN EXPRESSION ENDS
HERE
JUST BECAUSE
THE LINE DOES
NOT CONTAIN ANYTHING
ELSE
>
> Because TRUE means something in the context of PICK, it does
> not necessarily imply an ordinal ordering of logic values.
>
I listed a combination of factors, of which using logic! values
as selectors with Pick was only one.
>
> People usually think "TRUE" "FALSE", in that order.
>
What people? I know quite a few people that would happily agree
with the assertion that TRUE IS GREATER THAN FALSE.
>
> EITHER places the TRUE block first, the ELSE/FALSE block
> second. Pick thinks of TRUE/FALSE in that way.
>
I don't see EITHER and PICK as analogous. EITHER is not a
construct with varying numbers of blocks, nor can we use
positional numeric selection with EITHER; therefore we are
free to use a "linguistic" model instead of a numerical order
model for deciding how to arrange its parts.
"Either you pick up your room or you're grounded from
the football game tonight!"
as an equivalent of
If you do not pick up your room, then you're grounded.
If you do pick up your room [and fill up the car with gas
and refrain from all other annoying behavior, such as
playing the stereo to loudly...], then you may go to the
football game tonight.
where the ordering may be changed with enough verbal cueing.
>
> ... but now we have two clashing expectations if we consider
> them together (as you have done here)... The best you
> can do with things like this is to have the language be
> context dependent.
>
I agree, if we have done everything possible to eliminate
or resolve the clashes first. Even so, we should then choose
the context dependence so that the weakest analogy/expectation
is the one we break, rather than the strongest one!
>
> Should we change it so that we have:
> EITHER condition [FALSE-BLOCK] [TRUE-BLOCK]
> to accomplish some consistency with logic datatypes?
> NO WAY!
>
I quite agree! But I agree because of my previous statement;
the EITHER...PICK analogy is much weaker than the expectation
that converting among ordered data values should preserve their
ordering.
Actually, the underlying problem goes deeper. If we allowed
numeric labeling of elements in a collection to follow the
natural number progression of 0, 1, 2, ..., then it would be
even more obvious that FALSE selects the same labeled position
as 0 and TRUE selects the same labeled position as 1.
Before anyone responds with a claim that it's confusing for the
"first" position to be 0, let me point out that the "first"
house on my block is numbered (labeled) 1262! I have yet to
meet any of my neighbors (including children old enough to
read house numbers) who have any difficulty with interchanging
the relative position "first" with the numeric label "1262"
in routine conversation.
>
> Whenever we run into situations where we say "gee, that
> really dosn't work the way people would 'normally' expect"
> we fix them.
>
That sounds good. However, natural language and the common-sense
expectations of 'normal' people (or even programmers when they're
not programming) are notorious for their ambiguity and fuzziness.
I humbly propose that the need for precision in programming means
that rigorous, elegant, and minimalist definition should take
priority over everyday conversational comfort, should they ever
come into conflict.
>
> The thing I just want to exclude from the equation is the
> design of REBOL.
>
Unfortunately, as I mentioned above, that requires the assumption
that the design is already perfect. I happen to think that, on
the whole, it is very good and very interesting, but I'm hard
pressed ever to consider any product of human endeavor beyond
improvement.
>
> The odd cases where we do get bitten by apparent
> inconsistencies are far less than the ongoing experiences
> people have with REBOL where things DO work the way people
> expect them to.
>
Agreed. But to me that's even MORE motivation to work harder
"to reach the unreachable star" of perfection.
-jn-
--
; Joel Neely [EMAIL PROTECTED] 901-263-4460 38017/HKA/9677
REBOL [] do [ do func [s] [ foreach [a b] s [prin b] ] sort/skip
do function [s] [t] [ t: "" foreach [a b] s [repend t [b a]] t ] {
| e s m!zauafBpcvekexEohthjJakwLrngohOqrlryRnsctdtiub} 2 ]
--
To unsubscribe from this list, please send an email to
[EMAIL PROTECTED] with "unsubscribe" in the
subject, without the quotes.