People often suggest a bidirectional for, then demonstrate its readability on literal values -- and it does look good. The issue is that when you use variables or expressions (as is usually the case), it's not immediately obvious what the bounds (and thus behavior) of the loop will be. Hence, the separation.
Shouldn't 'forlen be used instead in your examples?
arc> (= xs '(a b c))
(a b c)
arc> (forlen i xs (prn (xs i)))
a
b
c
nil
But hey hey, too many loop constructs. If I hadn't `grep' Arc (see below), once again, I'd not know about it, too.
I don't agree with 'for_ being "buggy" or with the 'for/'down being an obvious/mandatory division. It's just a tradeoff in my opinion. But you're right, don't get me wrong, you're right there is a rational behind 'down.
The descendant loop being a separate concept because there are cases where it is "necessary", well... I actually, personally, don't buy this. I call "everyone repeats the lesson, and no one questions it".
It happens, it is rare but it happens, that I need a descendant 'for. And schtroumpf, I can never remember the syntax for it (in any language), I always need to Google it. But God knows I can remember a tremendous amount of details (last example in Arc: the need for 'write/'disp, understand it while coding evsrv, hop it's in my brain and it will not be forgotten. This kind of details, OK.). And because I think my brain is awesome, and only forget useless stuff (I remember "useful" things I saw when I was 5), well I don't buy the need for 'down.
For strange behaviours where bounds would be inversed, well, I always check the input where needed (general rule). If I'm doing "explanatory programming", well it's OK if this causes "bugs". It is far more OK than if I need to WTF and loose time, and my concentration, to start my browser, and ask Google how to 'down. And often, use 'forlen/'each. 'for is good for C.
And yes, all this is terribly arrogant. But I'm not alone in the "need to Google it every time".
The funnier is, I actually `grep'ed Arc files yesterday, and I'm nearly sure 'down could be removed without problems/'for_ adopted, according that you modify some things (like the def of 'forlen).
And I'm nearly sure it will be good, because Worse Is Not Always Better. The programmer should have the easy life (i.e: not having to remember 100 loop constructs), and not the {system|language} designer (which should make sure the def of say 'forlen is correct even for empty lists, even if it means adding an 'if or anything). I don't care too much if Arc.arc is a little bloated if it means I have less stuff to remember.
Where "good" here means, my definition of "good", and the crazy definition of "Would make Arc and news.arc shorter". Yes, I claim it'd actually make it shorter.
Unfortunately, ars longa, vita brevis, and I don't want to waste time to prove this rather useless point. But I'm nearly sure 'down is useless in this current small-not-so-small version of Arc + libs. Oh and schtroumpf, I add it to my ARROGANT_TODO list. Will demonstrate my point of view is at least very acceptable one of these days, so that you don't take me for a moron too much :-)
But of course, if you like 'for to be like this, I see no problem with this.
And thanks for taking the time to remind all this to me (because sincerely, one more time, I couldn't see why 'for in Arc couldn't go in descendant).
And anyway, 'for is so 70s. 'each, 'repeat are far more used. How many times do we use 'for directly (hint: something like 5 times in x.arc, macros definitions excluded because this doesn't count IMO, "worse is not always better", and most of the times when you are sure the bounds are ok, e.g: (for 0 255 ...))? And how many times do we use 'down (hint: once in x.arc)?!
----
Ultime arrogance, I'll quote Einstein here:
The important thing is not to stop questioning [the real need for 'down, even if everyone says so, when `grep' is far less convinced than people on this]. Curiosity has its own reason for existing.
Shouldn't 'forlen be used instead in your examples?
I'd say each should be used in the examples. The point is that they are easy instances of a more general problem, as I noted: when using for, you're typically using expressions; when you're using expressions, you aren't sure if the bounds will result in an ascending or descending loop. There are instances where this distinction is important. Take posmatch in strings.arc, defined as
(def posmatch (pat seq (o start 0))
(catch
(if (isa pat 'fn)
(for i start (- (len seq) 1)
(when (pat (seq i)) (throw i)))
(for i start (- (len seq) (len pat))
(when (headmatch pat seq i) (throw i))))
nil))
Here we see the else-clause for-loop isn't merely a place to substitute forlen or each: it only iterates up (by for's behavior) to the largest index at which the pattern could occur in the sequence:
That's fine, of course. It works. It just seems gratuitous -- like something I'd want handled for me already. But everyone has their definitions of "good", and yours is certainly no less (or more) valid than mine.
Hell, someone might like having the bidirectional loop in general, then use a separate loop ("up"?) for this case.
The programmer should have the easy life (i.e: not having to remember 100 loop constructs)
Whereas I think remembering 100 loop constructs is easier than remembering that the handful of loop constructs are incredibly fragile.
But of course, if you like 'for to be like this, I see no problem with this.
Nor do I see a problem if you want a bidirectional for. This is one use for macros: rather than worry that Arc doesn't have some loop construct, you're allowed to make your own. No need for the language spec to get updated if you can easily write a bidirectional loop. And if for was changed to be bidirectional, I could similarly write macros for ascending and descending loops.
As you say, this is just the rationale. But that's not saying much: by its very nature, language design is about rationale; the only "necessary" components of the language are basically the ones that make it Turing-complete.
Thanks my arrogance/guts for pushing me to try to remove 'down, because it showed me the Arc codebase confirms my own experience of programming:
- you never use 'for directly, but in cases where you are sure the bounds are OK.
Where "directly" means, not in a library {mac|fn} definition, because here you must anyway validate your input, if you agree w/ "Worse is not always better" (i.e: the {sys|lang|lib} writer does the hard work, not you, the user).
If you don't agree, well, one problem is, it leads to incoherences/bugs. See below.
The "problematic" (few) occurrences of 'for only appears in arc.arc and strings.arc which are typical librairies files. Not even "normal" librairies, but "core" ones.
The kind of ones were I'd strongly apply "worse is not better".
You'll not see 'for used with expressions in any other files, i.e: "application" (blog.arc, news.arc, etc.) or even other libs files.
You'll not even see it at all in news.arc, srv.arc, code.arc, prompt.arc.
You'll see it used directly twice, here:
blog.arc: (for i 0 4 ; no bounds pb
html.arc:(for i 0 255 (= (hexreps i ; no bounds pb
- you sometimes, rarely, also need to directly use a descendant 'for ('down). Only once in all Arc (but once = it is needed):
news.arc: (down id maxid* 1
Where maxid* is a global, and the kind of one which is nearer IMO to a litteral than to a (complex) expression, so no pb. See below.
So it's a pity that for this one time, you can't use 'for, and have to ressort using yet another loop construct that is here for... non-existing problems.
- for the vast, vast majority of looping, you use higher-level loop constructs (each/repeat/etc.), so there is no problem w/ incorrect bounds, assuming the lib writer is not a moron.
----
In arc.arc:
Is it coherent than 'posmatch will return
nil when pat > seq, where 'headmatch will
throw an error in the same case (even stranger knowing 'posmatch
actually calls 'headmatch)?
arc> (headmatch "abcd" "abc")
Error: "string-ref: index 3 out of range [0, 2] for string: \"abc\""
arc> (posmatch "abcd" "abc")
nil
Coherent, and correct IMO. We ask if it
matches. If pat > seq, the answer is just
"no", it's not an error per-se.
Or: how 'headmatch is "incredibly fragile",
and the so-called "solid" 'for hides this
fact here. Thanks pseudo-solidity.
Validate your input, and don't rely on the behaviour
of something inherently fragile (using a raw construct),
when writing a library fn.
In news.arc, I (obviously) changed:
(down id maxid* 1
to:
(for id maxid* 1
I feared it may not work when there are no item, tested this case
(nsv), then access localhost:8080, and there were actually no problem.
I don't use news.arc, so can't test for the rest, but it should be OK.
(If pb, maybe just changing to (for id maxid* 0 ...) would solve it.)
----
"You claimed it'd make the code shorter! Prove it!"
Clever, interesting test:
arc> (let toto 0
(each (k v) (tokcount '("arc.arc" "strings.arc" "news.arc"))
(++ toto v))
toto)
14756
arc-no-down> (let toto 0
(each (k v) (tokcount '("arc.arc" "strings.arc" "news.arc"))
(++ toto v))
toto)
14749
Harder, dumber, raw `wc' test:
$ wc -m 3.1orig/*.arc
[...]
198017 total
$ wc -m 3.1nodown/*.arc
[...]
198017 total # Argh, failed! It's ==, not strictly <...
----
No-down patch was coded quickly and with nearly no testing afterwards, so there might be bugs. I hope someone prouve me I've introduced lots of bugs, like this I could be sure all this crap at least makes someone take a look at the reality (where the reality is, here, some pratical code, and not some books), and try to question things. One thing Arc got very right is "code.arc".
And no, telling me "it is buggy for me" doesn't count without showing some Arc code, in where you'll be effectively embarrassed by the new 'for behaviour. Else it's like with hygienic macros: "incredibly less fragile" but no one cares 'cause unhygienic is good enough/more powerful, according you live in the real world.
And anyway it doesn't count because everyone here more or less accept the fact that the Arc codebase is a superb piece of software (so if you don't have the same coding practice, you suck), that brevity is power, and that it is a valid codebase to test the necessity of an operator. All of this IS questionable. But too many people here are... not qualified to do so, unless they are sure their comments history will not reveal some stupid blind adoration for Arc.
I trust {my|other people} guts & feelings, but on the end I believe only in reality, in data (and you know as well as me that code is data :-D), and not in opinions and books.
- you never use 'for directly, but in cases where you are sure the bounds are OK.
The "problematic" (few) occurrences of 'for only appears in arc.arc and strings.arc which are typical librairies files.
What makes arc.arc and strings.arc less valid examples of for usage? They're Arc programs, too. Should they not inherit the elegance they're attempting to define? (While still balancing efficiency, of course, cf. the tutorial: http://ycombinator.com/arc/tut.txt)
To the contrary, because arc.arc and strings.arc use for I think they make perfect examples -- which would make your first statement untrue, since you had to write extra bounds-checking.
- you sometimes, rarely, also need to directly use a descendant 'for ('down). Only once in all Arc (but once = it is needed):
So it's a pity that for this one time, you can't use 'for, and have to ressort using yet another loop construct that is here for... non-existing problems.
You're ignoring that down has another purpose. As you say, the need for a descending loop is rare. But the need for for to only go in one direction is much less rare (more on that later).
for the vast, vast majority of looping, you use higher-level loop constructs (each/repeat/etc.), so there is no problem w/ incorrect bounds, assuming the lib writer is not a moron.
So you'd also want to foist the responsibility of not being a "moron" onto every user of for? If other loops are already used to avoid silly bugs, why not for?
I count at least 12 different loop constructs in arc.arc: while, loop, for, down, repeat, each, whilet, whiler, forlen, on, until, noisy-each, and arguably others like evtil and drain.
I find that adding these makes code simpler: they express (and implement) purposeful loops. That's why I can do
(each x xs (prn x))
instead of
(forlen i xs (prn (xs i)))
which can be done instead of
(for i 0 (- (len xs) 1) (prn (xs i)))
which can be done instead of
(loop (= i 0) (< i (len xs)) (++ i) (prn (xs i)))
etc. If I wanted the most general & least to remember, I'd use a goto.
When for tries to infer the direction I want to go, I need to fight it to stop from going in the opposite direction -- to me, this is inconvenient.
Is it coherent than 'posmatch will return nil when pat > seq, where 'headmatch will throw an error in the same case (even stranger knowing 'posmatch actually calls 'headmatch)?
I agree that headmatch has odd behavior here. But with the fixed behavior (i.e., your patch):
Just because the function to which you funnel input sanitizes data doesn't mean you should be supplying bad values. Further, if we add more error-checking to posmatch to avoid the redundant calls, we're adding even more complexity -- wrestling against for to get it to go just one direction.
"You claimed it'd make the code shorter! Prove it!"
I believe only in reality, in data
Then let's inspect your patch closer:
inspect-patch.arc
(def default (file)
(+ "../arc3.1/" file))
(def patched (file)
(+ "../arc-patch/" file))
(def sexp-tokcount (sexp)
(len (flat sexp)))
(= for-def*
'(mac for (v init max . body)
(w/uniq (gi gm)
`(with (,v nil ,gi ,init ,gm (+ ,max 1))
(loop (assign ,v ,gi) (< ,v ,gm) (assign ,v (+ ,v 1))
,@body))))
down-def*
'(mac down (v init min . body)
(w/uniq (gi gm)
`(with (,v nil ,gi ,init ,gm (- ,min 1))
(loop (assign ,v ,gi) (> ,v ,gm) (assign ,v (- ,v 1))
,@body))))
new-for-def*
'(mac for (v init end . body)
(w/uniq (gi gm gt gf)
`(do
(if (> ,end ,init)
(= ,gt < ,gf +)
(= ,gt > ,gf -))
(with (,v nil ,gi ,init ,gm (,gf ,end 1))
(loop (assign ,v ,gi) (,gt ,v ,gm) (assign ,v (,gf ,v 1))
,@body))))))
; if this calculation is wrong, it should be revealed in logic-savings
(= max-diff* (- (+ (sexp-tokcount for-def*) (sexp-tokcount down-def*))
(sexp-tokcount new-for-def*)))
(def token-total (file)
(sum cadr (tokcount (list file))))
(def token-diff (file1 file2)
(- (token-total file1) (token-total file2)))
(def compare-tokcount (filename)
(let diff (token-diff (default filename) (patched filename))
(if (> diff 0)
(prn "The patch saved " (plural diff "token") " in " filename)
(< diff 0)
(prn "The patch added " (plural (- diff) "token") " to " filename)
(prn "The patch didn't change the token count in " filename))))
(def maximum-savings ()
(prn "The patch could have saved at most (caveat lector) "
(plural max-diff* "token")
" in arc.arc"))
(def logic-savings ()
(let diff (token-diff (default "arc.arc") (patched "arc.arc"))
(if (<= diff max-diff*)
(prn "So, by changing 'for in arc.arc, "
(plural (- max-diff* diff) "token")
" got added to code that used the previous version of 'for")
(err "miscalculated the maximum number of tokens you could save"))))
(map compare-tokcount '("arc.arc" "strings.arc" "news.arc"))
(prn)
(maximum-savings)
(logic-savings)
At the REPL
arc> (load "inspect-patch.arc")
The patch saved 9 tokens in arc.arc
The patch added 2 tokens to strings.arc
The patch didn't change the token count in news.arc
The patch could have saved at most (caveat lector) 17 tokens in arc.arc
So, by changing 'for in arc.arc, 8 tokens got added to code that used the previous version of 'for
nil
To explain the "caveat", I assume the most this new for could change is: (a) remove the single-direction for and down, (b) add the bidirectional for, and (c) leave any other piece of code that used for/down unchanged (save switching the word "down" to the word "for").
With these assumptions (and by inspecting the code), the assessment seems correct: arc.arc nets 8 additional tokens to stop for from going backwards. It's not that the token count is shorter from having for go both directions; it's that the code you've added to avoid for's new behavior isn't quite enough to outweigh the savings from removing down's definition.
In actuality, you'll wind up saving far less than 9 tokens because of multiple evaluation bugs:
i.e., 7 more tokens, totaling 14 more tokens, which outweighs the original figure. So, nothing is even really saved in arc.arc. Though, of course, the rewrites could be shorter with something like once-only (see towards the end of http://gigamonkeys.com/book/macros-defining-your-own.html).
Further, strings.arc and news.arc did not get shorter (strings.arc even got a little longer). The only way it seems that un-patched code could get shorter is if it had to go either up or down and the order didn't matter -- unlike code in the files inspected.
Therefore, this patch can either make new code longer or make you hope that for doesn't iterate in a direction you don't want it to (as in news.arc), unless you needed to do the Arc 3.1 equivalent of
(if (< start end)
(for i start end ...)
(> start end)
(for i end start ...))
which, with this patch, could be replaced with
(for i start end ...)
which is shorter.
As infrequently as such code occurs (0 times in the standard Arc 3.1 distribution, so far as I can tell), this does not yield big space savings. If it does occur frequently enough, it shouldn't outweigh the need for single-direction iterations, but would probably instead be made into a separate macro:
(mac between (var bound1 bound2 . body)
...)
Additionally, you assert that having an extra loop construct entails an unnecessary mental burden for the programmer. I disagree. It's not a burden if its purpose is specific: if you want to repeat a block of code, use
(repeat n ...)
instead of
(for temp 1 n ...)
If you want to iterate over the length of a sequence, use
(forlen i xs ...)
instead of
(for i 0 (- (len xs) 1) ...)
Moreover, if you want to iterate upwards through a range of integers, use
As you say, using write may be the thing to do. It's good to raise these questions, though, as there's no particularly "right" answer. write and disp compile to Scheme's write and display, for which R5RS gives the rationale (http://schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-9...):
Write is intended for producing machine-readable output and display is for
producing human-readable output. Implementations that allow "slashification"
within symbols will probably want write but not display to slashify funny
characters in symbols.
Where "slashification" looks like
arc> (do (write 'a\ b) (prn))
|a b|
nil
arc> (do (disp 'a\ b) (prn))
a b
nil
The quote issue raises consistency questions:
arc> (prn "quotes don't show here")
quotes don't show here
"quotes don't show here"
arc> (prn (list "so should they really show here...?"))
(so should they really show here...?)
("so should they really show here...?")
It makes sense that disp doesn't print the quotes in the second example: it doesn't in the first, and the output isn't meant to be read back in by the machine, unlike write's output. But there's nothing obviously wrong with it printing the quotes, either.
I'm pretty sure it was a statement of curiosity. I'd venture to say that that's the intention most of us would have in asking to see the source. (Hell, I'd be interested in seeing it.) You're probably reading too much into this.
(Plus, it's not really "stealing" if you willingly share the code.)
Sorry to be a pedant, but the paren-balancing is incomplete:
arc> (string #\()
; can't send, even if I click the button instead of hitting "enter"
arc> #\(
)
#\(
Error: UNKNOWN::4: read: unexpected `)'
arc> "("
)
"("
Error: UNKNOWN::4: read: unexpected `)'
I tried this first, since I've noticed that syntax highlighters (of all things) often get this wrong.
Beside that point, very nice job. I especially like the interface: it looks great. My (light) testing yields no other bugs, at this point.
Also, not a bug, but a suggestion (tested in Firefox 3.0.8):
You might consider doing some line-wrapping of the output. For instance, when I just do sig at the REPL, it prints out the (large) hash-table just fine, but it's on one line, so the frame's dimensions stretch to accommodate it. The textbox for the prompt then centers under this wide line, and it's awkward to side-scroll to type more code and read further output.
> Sorry to be a pedant, but the paren-balancing is incomplete
This is not pedantry, this is bug reporting!
Yes, the check for '(', ')'s is really naive, and to be honest, I don't feel like coding a decent Lisp parser in Javascript for now :-)
> can't send, even if I click the button instead of hitting "enter"
This was actually not a bug, but a desired feature :-D But this was totally stupid, since the '(' check is incorrect in some cases.
Now, clicking the button will anyway send the content, no check done in JS. If your content is invalid, evsrv will tell you:
arc-online> ( ; then click on the button
Error: UNKNOWN::0: read: expected a `)' to close `('
> You might consider doing some line-wrapping of the output.
Yes, absolutely. This is now (partially) done. `sig' and the majority of cases will be correctly handled, but not everything. `(for i 0 1000 (pr i))', for instance, is still "broken".
The problem is, the results are inserted in <pre> blocks, and after some search, it's easy to make them break lines on whitespaces, but not otherwise (CSS property "white-space").
Plus, a small bug I just recently noticed has been fixed: evaluating "nil" or "()" or "#f" didn't work. It now does ;-) But:
arc-online> #f
nil
arc> #f
#f
But remember this is the eval server:
arc> (eval '#f)
nil
So this is actually quite normal ;-)
Oh, and I tried the site today with Safari, and it globally works but the layout is bad. I'll fix this ASAP.
Anyway, thanks a lot fallintothis for your feedback! Testing these edge cases was smart of you ;-)
Does Arc have problems on MzScheme 4.1.2? Nothing in the changelog leaps at me immediately, but I'm not PLT-savvy:
Version 4.2.1, July 2009
Added string->unreadable-symbol and symbol-interned?
Added syntax-local-lift-provide
Added syntax-local-lift-values-expression
Added identifier-prune-lexical-context and quote-syntax/prune
Version 4.2, May 2009
Changed visiting of modules at phase N to happen only when compilation
at phase N starts
Changed expander to detect a reaname transformer and install a
free-identifier=? syntax-object equivalence
Changed provide to convert an exported rename transformer to its
free-identifier=? target
Added 'not-free-identifier=? syntax property to disable free-identifier=?
propagation through a rename transformer
Added prop:rename-transformer and prop:set!-transformer
Fixed scheme/local so that local syntax bindings are visible to later
local definitions
Changed current-process-milliseconds to accept a thread argument
Added hash-hash-key?, hash-ref!
Added in-sequences, in-cycle
Version 4.1.5, March 2009
Allow infix notation for prefab structure literals
Change quasiquote so that unquote works in value positions of #hash
Change read-syntax to represent #hash value forms as syntax
Added bitwise-bit-field
Version 4.1.4, January 2009
Changed memory accounting to bias charges to parent instead of children
Changed function contracts to preserve tail recursion in many cases
Added scheme/package, scheme/splicing, ffi/objc
Added exception-backtrace support for x86_84+JIT
Added equal?/recur
Added equal<%> and interface* to scheme/class
Added procedure-rename
Added extra arguments to call-with-continuation-prompt
Added extra argument to raise-syntax-error
Added compile-context-preservation-enabled
Added syntax-local-lift-require
Added internal-definition-context-seal, which must be used on an
internal-definition context before it's part of a fully expanded form
Added syntax-local-make-delta-introducer
Changed make-rename-transformer to accept an introducer argument that
cooperates with syntax-local-make-delta-introducer
Added internal-defininition-context?
Added identifier-remove-from-defininition-context
Version 4.1.3, November 2008
Changed scheme to re-export scheme/port
In scheme/port: added
Added call-with-immediate-continuation-mark
In scheme/port: added port->string, port->bytes, port->lines
port->bytes-lines, display-lines, [call-]with-input-from-{string,bytes},
and [call-]with-output-to-{string,bytes}
In scheme/file: added file->string, file->bytes, file->lines,
file->value, file->bytes-lines, write-to-file, display-to-file,
and display-lines-to-file
Added variable-reference? and empty #%variable-reference form
Extended continuation-marks to work on a thread argument
I had seemed to recall someone not having problems with an older 4.1.x version, but I didn't want to press my luck, and I didn't think testing Arc locally would be the right thing to do. And, as rntz points out, I don't want to use that provider anyway. Thanks for replying.
As for point 5, Arc uses certain characters as syntax abbreviations iff the special characters occur in what would otherwise be normal symbols (to the Scheme reader). So far, there's
.a ; is the same as (get a)
a.b ; is the same as (a b)
!a ; is the same as (get 'a)
a!b ; is the same as (a 'b)
f&g ; is the same as (andf f g)
f:g ; is the same as (compose f g)
~f ; is the same as (complement f)
where
((get a) b) ; is the same as (b a) by Arc's indexing
; e.g., ("abc" 0) is #\a
((andf f g) x) ; is the same as (and (f x) (g x))
((compose f g) x) ; is the same as (f (g x))
((complement f) x) ; is the same as (no (f x))
See arc.arc for the actual definitions of these operators.
There are precedence and associativity rules, such as
a!b.c ; is the same as ((a 'b) c) because ! and . are
; left-associative
f:g:h ; is the same as (compose f g h)
~f:g ; is the same as (compose (complement f) g)
To explore these more, you can use the ssexpand function in Arc:
arc> (ssexpand 'a:b.c!d)
(compose a b.c!d)
arc> (ssexpand 'b.c!d)
((b c) (quote d))
$ mzscheme -f as.scm
Use (quit) to quit, (tl) to return here after an interrupt.
arc> $$x
Error: "list->string: expects argument of type <list of character>; given #\\$"
arc> (= tbl (table))
#hash()
arc> (= tbl$x '(1 2 3))
Error: "reference to undefined identifier: _x"
arc> (= x "key")
"key"
arc> (= tbl$x '(1 2 3))
(1 2 3)
arc> tbl
#hash((key . (1 2 3 . nil)))
arc> tbl$x
(1 2 3)
arc> tbl$x.0
1
arc> (tbl 'x)
nil
arc> (tbl 'key)
(1 2 3)
arc> $x
key
arc> $x$y
Error: "reference to undefined identifier: _y"
arc> (= y "abc")
"abc"
arc> $x$y
Error: "Function call on inappropriate object key (abc)"
arc> :a
$ mzscheme -i -f as.scm
Welcome to MzScheme v4.2.1 [3m], Copyright (c) 2004-2009 PLT Scheme Inc.
Use (quit) to quit, (tl) to return here after an interrupt.
arc> :a
> (ac '$x$y '())
(ar-funcall1 (ar-funcall1 _sym (ar-funcall1 _sym _x)) (ar-funcall1 _sym _y)) ; buggy?
Scheme syntax isn't all that different from Arc syntax. The main differences you see in this example are:
- Instead of
(def f (x) body)
in Scheme it's
(define (f x) body)
- Different names: Arc is = Scheme eqv?, Arc t & nil = Scheme #t & #f, Arc fn = Scheme lambda, etc.
- Scheme's cond is like Arc's if, but with extra parentheses.
; Arc
(if a b
c d
e)
; Scheme
(cond (a b)
(c d)
(#t e))
- You have access to a different set of library functions than the ones Arc provides. Most of what you need is defined in ac.scm. If you're unsure how it works, puzzle at the definition some.
I'm not sure if you were asking for help, but I spent awhile hacking the program into a working state. I hope this write-up is useful as a way of getting familiar with Arc.
I started with the original file and debugged slowly. I'll try my best to go step-by-step, illustrating my thought-process. Seemed best to start from the REPL (read-eval-print-loop).
arc> (load "game-of-life.arc")
Error: "reference to undefined identifier: _tbl"
I'm not sure if you defined a different file (or modified ac.scm or such) so that tbl is a function, but I assume you meant table:
(= universe* (tbl)) ; changed this
(= universe* (table)) ; to this
arc> (load "game-of-life.arc")
Error: "Can't take cdr of x1"
I notice you have a particular with statement. Unlike let, which is for a single variable name, with is for several variables. As such, you need to wrap the variable declarations in an extra set of parentheses. I.e., instead of
Hm. Not the clearest message. I see the hash-tables have the same fields as the cell template. What the error means is that there's some code that looks like
(= (findfellas cell) cell)
After some searching, it turns out that
(def create-cell (x y)
(= c (inst 'cell 'x x 'y y 'state 0 'fellas () 'index (cindex x y))
(findfellas c)
c))
has some unbalanced parentheses. The (findfellas c) and c are both still inside of the =. The way = works is to pair up alternating variables/values, like:
arc> (= x 1 y 2)
2
arc> x
1
arc> y
2
So, in effect, this is trying to do
(= (findfellas c) c)
Rebalance the parens
(def create-cell (x y)
(= c (inst 'cell 'x x 'y y 'state 0 'fellas () 'index (cindex x y)))
(findfellas c)
c)
In the REPL again,
arc> (load "game-of-life.arc")
Error: "reference to undefined identifier: _x"
Well, where's that coming from? I search the source code for some unbound x. create-cell and cindex are okay, so the only other place is in findfellas, where we find
(with x1 (- c.x 1) x2 c.x x3 (+ c.x 1)
As rtnz noted,
c.x
expands into
(c x)
which tries to look up the variable x, which fails. What you want to access the fields of the template is
c!x
which expands into
(c 'x)
Notice the single-quote mark. This is a symbol. Symbols are covered in the Arc tutorial, so I'll spare you, but they have interesting uses I'll get back to later.
Similar instances of a.b instead of a!b produce the errors
Error: "reference to undefined identifier: _y"
Error: "reference to undefined identifier: _fellas"
i.e., of the form (sequence index). Here, you have (index sequence). Oops! Reverse the order, and we're good.
(let pos (cindex i j)
(= (u pos) (create-cell i j)))))
A couple of things to note: first, that Arc is 0-indexed. Some looks at your code reveal for-loops that start at 1. I don't know if they're doing anything "wrong" at this point, but in case you didn't know
(xs 0) ; == the *first* element of xs
(xs 1) ; == the second element of xs
; etc.
Also, it's desirable to generally (but not always) avoid naming variables the same thing as built-in functions & macros. Arc defines pos to find the position of an item in a sequence:
arc> (pos #\a "cbabc")
2
By rebinding pos, you clobber this definition (for more info, you can Google "Lisp-1 vs Lisp-2"). Here, it doesn't hurt though, except perhaps for readability.
In the REPL again,
arc> (load "game-of-life.arc")
Error: "random: expects type <pseudo-random-generator> as 2nd argument, given: 30; other arguments were: 1"
Well, this is a weird error! If you aren't sure whether there's a random function in Arc, you can check:
arc> (sig 'random)
nil
arc> random
Error: "reference to undefined identifier: _random"
So, Arc doesn't have a function named "random". Its random-number function is rand. The error says something about a "2nd argument", so let's try:
arc> (rand 1 50)
Error: "random: expects type <pseudo-random-generator> as 2nd argument, given: 50; other arguments were: 1"
Aha. rand doesn't like taking a 2nd argument. What's happening is that Arc's rand gets compiled down to Scheme's random. In your code, there's
(let cpos (rand 1 (len u))
Ouch. Maybe 0-indexing would help here. I make a comment of this potential issue, and sweep it under the rug by just:
(let cpos (rand (len u)) ; XXX should be 0-indexed?
In the REPL again,
arc> (load "game-of-life.arc")
Error: "reference to undefined identifier: _state"
Seeing that state is used as a field of the cell template, I suspect the same a.b vs a!b issue, and find the lines
meaning that it's happening when rejuvenate is called and c is nil. Lacking a debugger, I litter the program with print-statements to see what's happening.
; what's being passed to rejuvenate?
(def rejuvenate (c) (prn "c: " c) (= c!state 1))
(def init-universe (u)
(prn (sort < (accum a ; make accumulator called a, sort & print its result
(for i 1 x*
(for j 1 y*
(let pos (cindex i j)
(a pos) ; accumulate pos into the resulting list
(= (u pos) (create-cell i j))))))))
(prn "(len u) = " (len u)) ; what's the maximum random number to generate?
(repeat 10 ; XXX not guaranteed to produce 10 live cells due to duplicates!
(let cpos (rand (len u))
(prn "cpos: " cpos) ; what index are we using?
(prn "u.cpos: " u.cpos) ; what is the value at that index?
(rejuvenate u.cpos))))
Running the program, I get the following in the output:
So, nil is indeed being passed to rejuvenate, since (as we can see in the list of results of cindex) u.17 doesn't exist. The root problem here is then cindex -- as can be seen, it generates many duplicate numbers.
I inspect cindex and its use for awhile, coming to the conclusion that you're trying to represent the 2D Conway's Game of Life board in a 1D data structure. That is, instead of being able to reference a position by two points (like in a Cartesian plane), you're trying to condense it into a single point.
I thought it'd be clearer to represent the board as a 2D structure by just nesting a list within a list. This is alright; it remains sorted, but access times are generally slower than hash-tables and a lot of the code relies on iterating over integers. For better or worse, I decided to start rewriting code (trying my best to preserve your original code) to use a nested table. In so doing, there were other parts to refactor.
I look to see if you use the 'index field. Doesn't seem to be in use, so I get rid of it.
You've established 1 as a sentinel value for "alive" and 0 for "dead". Being in Lisp, this is a great place to use one of its interesting data literals: the symbol. It gives us the cheap ability to have data values that are simply a blob of text: these work great for sentinel values. Instead of 0/1, I rewrite it to use 'dead and 'alive. To see this in action, in the REPL:
arc> (= tbl (table))
#hash()
arc> (= (tbl 'x) 'alive) ; the symbol 'x is the key and the symbol 'alive is the value
alive
arc> tbl!x ; == (tbl 'x)
alive
arc> tbl.x ; == (tbl x) == tries to use the value in variable x as a key
Error: "reference to undefined identifier: _x"
arc> (= some-variable 'x)
x
arc> tbl.some-variable ;== (tbl some-variable) == (tbl 'x)
alive
arc> tbl!some-variable ;== (tbl 'some-variable) which is not a key in the table yet
nil
To see if this can be further simplified, I go to create-cell, the main place it's used.
Here, the 'x and 'y fields are set. I wonder how they're used, so look through the rest of the code. Turns out it's only in findfellas. But we needn't use = to set values all over the place that we access only once. We can redefine findfellas to work on just some coordinates x and y. More on that later. For the time being, I simplify with
Note the changes in alive? and dead? from isa to is. isa is defined in Arc as:
(def isa (x y) (is (type x) y))
But we don't want to check the type of the cell, just whether the state field is equal to something. For this, we use is.
Setting this aside for a moment, I try to remove the need for cindex from init-universe. I notice its reliance on using = to update the u that's passed. But assignment is kind of shaky business. Sometimes variables won't update the way you might expect them to (cf. passing by copy versus reference):
arc> (= x 1)
1
arc> (def f (something) (= something 1000))
#<procedure: f>
arc> (f x)
1000 ; sets "something" to 1000 and returns it
arc> x
1 ; but x is unmodified
arc> (= hash-table (table))
#hash()
arc> (def f (some-hash) (= some-hash!key 1000))
*** redefining f
#<procedure: f>
arc> (f hash-table)
1000 ; sets the key in "some-hash" to 1000
arc> hash-table
#hash((key . 1000)) ; and destructively updates the hash-table we passed in!
So, my solution is to use init-universe to generate a whole new object, then just return that object. Here's where the 0-indexing and nested hash-tables come into play.
(def init-universe ()
(let universe (table)
(for i 0 (- x* 1)
(= universe.i (table))
(for j 0 (- y* 1)
(= universe.i.j (create-cell i j))))
(repeat 10 ; not guaranteed to produce 10 live cells due to duplicates!
(with (i (rand x*)
j (rand y*))
(rejuvenate universe.i.j)))
universe))
But here, I use rejuvenate, which is itself setting a variable's value. It seems okay, though, because it's just setting a key in a hash-table, which (as seen above) should work:
Since I've been avoiding it, I turn back to findfellas. Instead of setting a cell's values, it should now just take in the things it needs to generate said values. Altering slightly yields
All of these if statements are introducing duplicated logic (plus, there's an unbalanced parenthesis in the last one). And now that cindex isn't used, we just need the two coordinates. Abstracting a bit, I got
(def findfellas (x y)
(with (x1 (- x 1) x2 x x3 (+ x 1)
y1 (- y 1) y2 y y3 (+ y 1))
(rem not-in-universe
(list (list x1 y1) (list x1 y2) (list x1 y3)
(list x2 y1) (list x2 y3)
(list x3 y1) (list x3 y2) (list x3 y3)))))
This let me sit back and figure out that it means to not be a valid coordinate. The best I could come up with was
; note: 0-indexing
(def not-in-universe ((x y))
(or (< x 0)
(< y 0)
(>= x x*)
(>= y y*)))
This definition makes use of a "destructuring" parameter, as it's called. To see the difference, here's a REPL interaction:
arc> (def f (x y) (prn "x: " x " y: " y) nil)
#<procedure: f>
arc> (f 1 2) ; called on two separate values
x: 1 y: 2
nil
arc> (def g ((x y)) (prn "x: " x " y: " y) nil)
#<procedure:zz>
arc> (g (list 1 2)) ; called on ONE parameter, a list, which is matched
; (or "destructured") against (x y). So (x y) == (1 2)
; implies that x == 1 & y == 2
x: 1 y: 2
nil
Okay. Not so bad. Now with a different representation of the universe, dumpu should be changed. This part is fairly straightforward:
(def dumpu (u)
(for i 0 (- x* 1)
(for j 0 (- y* 1)
(let c u.i.j ; u.i.j == ((u i) j)
(pr (if (alive? c) 1 (dead? c) 0)
"\t")))
(prn))) ; after every row, print out a newline
Phew! That seems to be the bulk of it. So, I try out the code.
arc> (load "game-of-life.arc")
nil
Oops. I forgot to check the main game loop. It's currently
I've rewritten init-universe to be non-destructive, so that needs to be changed. I also remember your comment about infinite numbers, but notice that your loop is until. In Arc, this is a loop that iterates "until" the expression is true (true values in Arc being anything other than the empty list). e.g.,
arc> (= x 5)
5
arc> (until (is x 0) (-- x) (prn x))
4
3
2
1
0
nil
You either want to use a different loop or use a boolean to signal dooms-day. But using a boolean that's not updated won't help -- either it's an infinite loop, or it just won't run at all. So, I reckon the looping construct should change, and the dooms-day counter should be a number. It makes sense to use repeat. So far, that's
I see the extra set of parens around the truth-clause of the if statement. I assume (and it makes sense) that you want to do both actions if (doomed? u) is true. For this, Arc has either the do block
arc> (do (prn "hi") (prn "how") (prn "are") (prn "you?"))
hi
how
are
you?
"you?"
and the when statement, which is like (if test (do ...))
arc> (when (> 10 5) (prn "I get executed") (prn "So do I.") (+ 1 2))
I get executed
So do I.
3
But then, calling (gof) again if we're doomed simply repeats the game until you stop it by force. I don't want to have to do that while testing, so I use one of Arc's more "advanced" constructs, point. If you don't know about continuations, basically what point does is give you a return statement (like in other languages), except you can name it whatever you want.
arc> (point return
(while t
(prn "in an infinite loop!")
(return 5)))
in an infinite loop!
5
arc> (point rabbit
(while t
(prn "wait, is this an infinite loop?")
(rabbit)))
wait, is this an infinite loop?
nil
So, in all, I've rewritten gof as
(def gof ()
(let u (init-universe)
(point return
(repeat doomsday*
(dumpu u)
(repeat 80 (pr "-")) ; print a line to help read between each gen
(prn)
(when (doomed? u)
(prn "we will start over")
(return))
(nextgen u)))))
Now all that's left is to rewrite doomed? and nextgen. The former is easy enough using the techniques applied thus far:
(def doomed? (u)
(point return
(for i 0 (- x* 1)
(for j 0 (- y* 1)
(if (alive? u.i.j)
(return nil)))) ; someone's alive, we're not doomed
t)) ; if we didn't find anyone who's alive, we're doomed!
nextgen is trickier. Even cleaning up the minor errors in your main conditional, it has overlapping logic:
(let goodfellas (len (alive-neighbours c u))
(if (alive? c)
; any live cell with fewer than two live neighbours dies
; any live cell with more than three live neighbours dies
; otherwise, live cell lives
(if (or (< goodfellas 2) (> goodfellas 3))
(takelife c))
; any dead cell with exactly three live neighbours becomes live
(if (is goodfellas 3)
(rejuvenate c))))
And once more I need to iterate with a nested for-loop across the universe. This is where I get sick and write a macro:
(Lightly tested, etc.) I'll leave it to you to figure out how it works. ;)
So
(def nextgen (u)
(each-cell c u
(let goodfellas (len (alive-neighbours c u))
(if (alive? c)
(if (or (< goodfellas 2) (> goodfellas 3))
(takelife c))
(if (is goodfellas 3)
(rejuvenate c))))))
This is a little bit freaky because of the assignment to a variable I've passed into the function. But, it (should be) setting to an index of a hash-table, so once more it looks fine.
I also clean up some other code using each-cell. But I can't use it for all cases, so there's probably a better macro to be written. Instead, I live with it; whatever.
In rewriting nextgen, I notice I need to rewrite alive-neighbours. (I've already taken the liberty of adding a hyphen to make the name easier to read.) This is pretty straightforward, again using the techniques described thus far:
I think that's it. I cleaned up the code as much as I'm going to (getting rid of the global universe, etc.), then tried it out on a 4x4 board for 3 generations:
I'm not sure if it's entirely correct, but it looks pretty good.
Sorry if any part of this seems condescending; I have literally no idea what your experience level is with programming or Arc. I just thought a write-up like this could help if someone wanted to see the (well, my) thought process behind tinkering with Arc code. Also, I've spent way too long writing this, so there are bound to be many typos. I'm not checking too intently because it's so long and I'm not going to make it perfect.
Note that this isn't the "best" or "right" way to write this code; merely the crack I took at it. Programming is an art.
Be that as it may, for reference I've posted the final version of my efforts at http://paste2.org/p/366945.
Thank you for writing such a long text. I sure could have avoided most of the syntax errors if had run it at least for once, but it serves me right by not doing so. This is my first arc program, but I think if I get such valuable comments, soon I will be writing good, idiomatic Arc code.