Mailing List Archive: 49091 messages
  • Home
  • Script library
  • AltME Archive
  • Mailing list
  • Articles Index
  • Site search
 

[REBOL] Re: Antwort: Re: WYSIWYG programming

From: joel:neely:fedex at: 27-Oct-2000 23:06

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. [jeff--rebol--net] 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 destroy