Arc Forumnew | comments | leaders | submit | fallintothis's commentslogin

The concern here being subtle bugs like

  (for_ i 0 (- (len xs) 1) 
    (prn (xs i)))
This looks alright:

  arc> (= xs '(a b c))
  (a b c)
  arc> (for_ i 0 (- (len xs) 1) (prn (xs i)))
  a
  b
  c
  nil
but

  arc> (= xs nil)
  nil
  arc> (for_ i 0 (- (len xs) 1) (prn (xs i)))
  Error: "Function call on inappropriate object nil (0)"
since

  arc> (for_ i 0 (- (len xs) 1) (prn i))
  0
  -1
  nil
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.

-----

1 point by palsecam 5931 days ago | link

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.

-----

2 points by fallintothis 5930 days ago | link

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:

  arc> (load "trace.arc") ; see http://arclanguage.org/item?id=10372
  nil
  arc> (trace posmatch headmatch)
  *** tracing posmatch
  *** tracing headmatch
  nil
  arc> (= trace-indent* 2)
  2
  arc> (posmatch "a" "abc")
  1. Trace: (posmatch "a" "abc")
    2. Trace: (headmatch "a" "abc" 0)
    2. Trace: headmatch ==> t
  1. Trace: posmatch ==> 0
  0
  arc> (posmatch "abc" "a")
  1. Trace: (posmatch "abc" "a")
  1. Trace: posmatch ==> nil
  nil
As a drop-in replacement, the bidirectional variant would fail:

  arc> (untrace posmatch)
  *** untracing posmatch
  nil
  arc> (mac for_ (v init end . body)
         (w/uniq (gv gi ge gt gf)
           `(do
              (if (> ,end ,init)
                (= ,gt < ,gf +)   ; classic, "ascendant", 'for
                (= ,gt > ,gf -))  ; 'down
              (with (,gv nil ,gi ,init ,ge (,gf ,end 1))
                (loop (assign ,gv ,gi) (,gt ,gv ,ge) (assign ,gv (,gf ,gv 1))
                  ((fn (,v) ,@body) ,gv))))))
  #(tagged mac #<procedure: for_>)
  arc> (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))
  *** redefining posmatch
  #<procedure:zz>
  arc> (trace posmatch)
  *** tracing posmatch
  nil
  arc> (posmatch "a" "abc")
  1. Trace: (posmatch "a" "abc")
    2. Trace: (headmatch "a" "abc" 0)
    2. Trace: headmatch ==> t
  1. Trace: posmatch ==> 0
  0
  arc> (posmatch "abc" "a")
  1. Trace: (posmatch "abc" "a")
    2. Trace: (headmatch "abc" "a" 0)
  Error: "string-ref: index 1 out of range for empty string"
For strange behaviours where bounds would be inversed, well, I always check the input where needed (general rule).

Your point, if I understand it, is that you'd rather catch this behavior in the logic of the code, e.g.,

  (if (and (>= (len seq) (len pat))
           (<= start (- (len seq) (len pat))))
    (for_ i start (- (len seq) (len pat))
      ...))
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.

-----

1 point by palsecam 5929 days ago | link

No-down patch at http://dabuttonfactory.com/res/arc-no-down.patch

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
w/no-down-patch:

  arc> (headmatch "abcd" "abc")
  nil
  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.

-----

2 points by fallintothis 5928 days ago | link

- 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):

  arc> (load "../arc3.1/trace.arc")
  nil
  arc> (trace posmatch headmatch)
  *** tracing posmatch
  *** tracing headmatch
  nil
  arc> (posmatch "a" "abc")
  1. Trace: (posmatch "a" "abc")
  2. Trace: (headmatch "a" "abc" 0)
  2. Trace: headmatch ==> t
  1. Trace: posmatch ==> 0
  0
  arc> (posmatch "abc" "a")
  1. Trace: (posmatch "abc" "a")
  2. Trace: (headmatch "abc" "a" 0)
  2. Trace: headmatch ==> nil
  2. Trace: (headmatch "abc" "a" -1)
  2. Trace: headmatch ==> nil
  2. Trace: (headmatch "abc" "a" -2)
  2. Trace: headmatch ==> nil
  1. Trace: posmatch ==> nil
  nil
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:

   (mac repeat (n . body)
     `(if (> ,n 1) (for ,(uniq) 1 ,n ,@body)))
with

  arc> (sexp-tokcount '(mac repeat (n . body)
                         `(if (> ,n 1) (for ,(uniq) 1 ,n ,@body))))
  18
should be

  (mac repeat (n . body)
    (w/uniq gn
      `(let ,gn ,n (if (> ,gn 1) (for ,(uniq) 1 ,gn ,@body)))))
with

  arc> (sexp-tokcount '(mac repeat (n . body)
                         (w/uniq gn
                           `(let ,gn ,n
                              (if (> ,gn 1) (for ,(uniq) 1 ,gn ,@body))))))
  25
i.e., 7 more tokens, and

  (mac forlen (var s . body)
    `(unless (empty ,s)
       (for ,var 0 (- (len ,s) 1) ,@body)))
with

  arc> (sexp-tokcount '(mac forlen (var s . body)
                         `(unless (empty ,s)
                            (for ,var 0 (- (len ,s) 1) ,@body))))
  21
should be

  (mac forlen (var s . body)
    (w/uniq gs
      `(let ,gs ,s
         (unless (empty ,gs)
           (for ,var 0 (- (len ,gs) 1) ,@body)))))
with

  arc> (sexp-tokcount '(mac forlen (var s . body)
                         (w/uniq gs
                           `(let ,gs ,s
                              (unless (empty ,gs)
                                (for ,var 0 (- (len ,gs) 1) ,@body))))))
  28
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

  (for i start (- (len seq) (len pat)) ...)
instead of

  (if (and (>= (len seq) (len pat))
           (<= start (- (len seq) (len pat))))
      (between i start (- (len seq) (len pat))
        ...))

-----


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.

-----

2 points by fallintothis 5935 days ago | link | parent | on: Your own Arc REPL, online

Wow. A bit of a heavy-handed analysis.

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.)

-----

4 points by fallintothis 5938 days ago | link | parent | on: Your own Arc REPL, online

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.

-----

4 points by palsecam 5938 days ago | link

> 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 ;-)

-----

2 points by fallintothis 5940 days ago | link | parent | on: Arc hosting providers?

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

-----

1 point by zck 5940 days ago | link

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.

-----

4 points by fallintothis 5941 days ago | link | parent | on: Question: What are Arc's strengths?

First-class continuations & call/cc are notoriously difficult to explain (and indeed, can be hard to understand in code that uses them). Your best bet is to Google around for info, seeing if you can make sense of the (vast) material available. http://en.wikipedia.org/wiki/Call-with-current-continuation and http://community.schemewiki.org/?call-with-current-continuat... might be good starts.

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))

-----

2 points by fallintothis 5944 days ago | link | parent | on: Mapa-b

Though in vanilla Arc

  arc> (sig 'range)
  (start end)

-----


You can build a new expand-symbolize (or whatever) and model it after expand-sexpr, since their functions are similar.

  $ diff -u old-ac.scm ac.scm
  --- old-ac.scm  2009-08-07 17:07:47.235041122 -0700
  +++ ac.scm      2009-08-07 17:07:52.789134895 -0700
  @@ -68,6 +68,7 @@                                  
          (or (let ((c (string-ref string i)))       
                (or (eqv? c #\:) (eqv? c #\~)        
                    (eqv? c #\&)                     
  +                 (eqv? c #\$)                     
                    ;(eqv? c #\_)                    
                    (eqv? c #\.)  (eqv? c #\!)))     
              (has-ssyntax-char? string (- i 1)))))  
  @@ -90,6 +91,7 @@                                  
     ((cond ((or (insym? #\: sym) (insym? #\~ sym)) expand-compose)
            ((or (insym? #\. sym) (insym? #\! sym)) expand-sexpr)  
            ((insym? #\& sym) expand-and)                          
  +         ((insym? #\$ sym) expand-symbolize)                    
        ;   ((insym? #\_ sym) expand-curry)                        
            (#t (error "Unknown ssyntax" sym)))                    
      sym))                                                        
  @@ -167,6 +169,14 @@                                             
                                   #t))                            
                  sym))                                            
                                                                   
  +(define (expand-symbolize sym)                                  
  +  (build-symbolize (reverse (tokens (lambda (c) (eqv? c #\$))   
  +                                    (symbol->chars sym)         
  +                                    '()                         
  +                                    '()                         
  +                                    #t))                        
  +                   sym))                                        
  +                                                                
   (define (build-sexpr toks orig)                                 
     (cond ((null? toks)                                           
            'get)                                                  
  @@ -180,6 +190,20 @@                                             
                          (err "Bad ssyntax" orig)                 
                          (chars->value (car toks))))))))          
                                                                   
  +(define (build-symbolize toks orig)
  +  (cond ((null? toks)
  +         'sym)
  +        ((null? (cdr toks))
  +         (chars->value (car toks)))
  +        (#t
  +         (list (build-symbolize (cddr toks) orig)
  +               (if (eqv? (cadr toks) #\$)
  +                   (list 'sym (chars->value (car toks)))
  +                   (if (eqv? (car toks) #\$)
  +                       (err "Bad ssyntax" orig)
  +                       (chars->value (car toks))))))))
  
   (define (insym? char sym) (member char (symbol->chars sym)))
  
   (define (symbol->chars x) (string->list (symbol->string x)))
Lightly tested:

  $ 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.

-----

2 points by thaddeus 5932 days ago | link

BTW I just got a chance to look at this tonight. I changed it a little so that $ would work with '!' and '.'

Again thanks for the help! T.

  (define (has-ssyntax-char? string i)
    (and (>= i 0)
         (or (let ((c (string-ref string i)))
               (or (eqv? c #\:) (eqv? c #\~) 
                   (eqv? c #\&)
                   (eqv? c #\$)                
                   ;(eqv? c #\_) 
                   (eqv? c #\.)  (eqv? c #\!)))
             (has-ssyntax-char? string (- i 1)))))


  (define (expand-ssyntax sym)
    ((cond ((or (insym? #\: sym) (insym? #\~ sym)) expand-compose)
           ((or (insym? #\. sym) (insym? #\! sym)(insym? #\$ sym)) expand-sexpr)
           ((insym? #\& sym) expand-and)
     ;     ((insym? #\_ sym) expand-curry)
           (#t (error "Unknown ssyntax" sym)))
     sym))

  (define (expand-sexpr sym)
    (build-sexpr (reverse (tokens (lambda (c) (or (eqv? c #\.) (eqv? c #\!)(eqv? c #\$)))
                                  (symbol->chars sym)
                                  '()
                                  '()
                                  #t))
                 sym))


  (define (build-sexpr toks orig)
    (cond ((null? toks)
           'get)
          ((null? (cdr toks))
           (chars->value (car toks)))
          (#t
           (list (build-sexpr (cddr toks) orig)
                 (cond ((eqv? (cadr toks) #\!)
                        (list 'quote (chars->value (car toks))))
                       ((eqv? (cadr toks) #\$)
                        (list 'sym (chars->value (car toks))))
                       ((or (eqv? (car toks) #\.) (eqv? (car toks) #\!)(car toks) #\$))
                         (err "Bad ssyntax" orig))
                       (#t (chars->value (car toks))))))))

-----

1 point by thaddeus 5945 days ago | link

That's great - thank you.

It'll take me a while to step through this, but I appreciate the guidance.

T.

-----

10 points by fallintothis 5946 days ago | link | parent | on: Show Arc: first program

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

  (let x 5 ...)
you do

  (with (x 5 y 10) ...)
In your code, then

  (with x1 (- c.x 1) x2 c.x x3 (+ c.x 1)
     y1 (- c.y 1) y2 c.y y3 (+ c.y 1)
becomes

  (with (x1 (- c.x 1) x2 c.x x3 (+ c.x 1)
         y1 (- c.y 1) y2 c.y y3 (+ c.y 1))
In the REPL again,

  arc> (load "game-of-life.arc")
  Error: "Can't set reference  #<procedure: findfellas> #hash((x . 1) (y . 1) (state . 0) (index . 1)) #hash((x . 1) (y . 1) (state . 0) (index . 1))"
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"
This is fixed by changing

  (with (x1 (- c.x 1) x2 c.x x3 (+ c.x 1)
         y1 (- c.y 1) y2 c.y y3 (+ c.y 1))
to

  (with (x1 (- c!x 1) x2 c!x x3 (+ c!x 1)
         y1 (- c!y 1) y2 c!y y3 (+ c!y 1))
and

  (= c.fellas (list (cindex x1 y1) (cindex x1 y2) (cindex x1 y3)
to

  (= c!fellas (list (cindex x1 y1) (cindex x1 y2) (cindex x1 y3)
In the REPL again,

  arc> (load "game-of-life.arc")
  Error: "Can't set reference  1 #hash() #hash((x . 1) (y . 1) (fellas . (1 1 2 1 2 2 2 4 . nil)) (state . 0) (index . 1))"
This looks like the error we got before. The pattern is that

  Error: "Can't set reference a b c"
means you have code of the form

  (= (a b) c)
Such that a doesn't define a setter (see arc.arc for defset implementation). In this case, it's

  (= (1 #hash()) ...)
This happens at evaluation, though, so we can't find a literal 1 that's being called on some object. Turns out there's the following let-block:

  (let pos (cindex i j)
    (= (pos u) (create-cell i j)))))
where you're assigning pos to some number, then trying to call it on u. To index sequences in Arc, you do something like

  arc> (= xs (list 1 2 3))
  (1 2 3)
  arc> (xs 0)
  1
  arc> (xs 1)
  2
  arc> (xs 2)
  3
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

  (pr c.state "\t")
and

  (def alive? (c) (isa c.state 1))
  (def dead? (c) (isa c.state 0))
  (def rejuvenate (c) (= c.state 1))
  (def takelife (c) (= c.state 0))
Again, change the dots to exclamation marks.

In the REPL again,

  arc> (load "game-of-life.arc")
  Error: "Can't set reference  nil state 1"
Yikes. By changing the code to

  (def rejuvenate (c) (= c!state 1))
  (def takelife (c) (= c!state 0))
it's now causing an error of the form

  (= (nil 'state) 1)
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:

  (1 2 2 3 3 4 4 4 5 5 6 6 6 6 7 7 8 8 8 8 9 10 10 12 12 12 12 14 14 15 15 16 16 16 18 18 20 20 21 21 24 24 24 24 25 28 28 30 30 32 32 35 35 36 40 40 42 42 48 48 49 56 56 64)
  (len u) = 30
  ; ...
  cpos: 17
  u.cpos: nil
  c: nil
  Error: "Can't set reference  nil state 1"
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.

First things first, you have a cell defined by

  (deftem cell
    'x nil
    'y nil
    'state nil ; 1 - alive, 0 - dead
    'fellas nil
    'index nil
    )
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
So far, cell is down to:

  (deftem cell
    'x nil
    'y nil
    'state  nil  ; 'dead or 'alive
    'fellas 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

  (deftem cell
    'state  nil  ; 'dead or 'alive
    'fellas nil)

  (def alive? (c) (is c!state 'alive))
  (def dead?  (c) (is c!state 'dead))

  (def create-cell (x y)
    (inst 'cell 'state 'dead 'fellas (findfellas x y)))
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:

  (def rejuvenate (c) (= (c 'state) 'alive))
  (def takelife   (c) (= (c 'state) 'dead))
Note how (c 'state) is the same as c!state.

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

  (def findfellas (x y)
    (with (x1 (- x 1) x2 x x3 (+ x 1)
           y1 (- y 1) y2 y y3 (+ y 1))
      (if (< x1 1) (= x1 1))
      (if (> x3 x*) (= x3 x*))
      (if (< y1 1) (= y1 1))
      (if (> y3 y*) (= y3 y*)
       (list (cindex x1 y1) (cindex x1 y2) (cindex x1 y3)
        (cindex x1 y1) (cindex x2 y3)
        (cindex x3 y1) (cindex x3 y2) (cindex x3 y3)))))
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

  (def gof ()
    (init-universe universe*)
    (until doomsday*
      (dumpu universe*)
      (nextgen universe*)
      (if (doomed? u) ((prn "we will start over")
              (gof)))))
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

  (def gof ()
    (let u (init-universe)
      (repeat doomsday*
        (dumpu u)
        (nextgen u)
        (if (doomed? u) ((prn "we will start over")
                (gof)))))
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 (aliveneighbours c))
       (if (isalive c)
           (if (or (< goodfellas 2) (in goodfellas 2 3)) ; == (<= goodfellas 3)
               (takelife c))
           (if (is goodfellas 3)
               (rejuvenate c))))
I rewrote it thus

  (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:

  (mac each-cell (cell universe . body)
    (w/uniq (i j u)
      `(let ,u ,universe
         (for ,i 0 ,(- x* 1)
           (for ,j 0 ,(- y* 1)
             (let ,cell ((,u ,i) ,j)
               ,@body))))))
(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:

  (def alive-neighbours (c u)
    (keep (fn ((x y)) (alive? u.x.y))
          c!fellas))
keep is described in the Arc tutorial.

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:

  arc> (load "game-of-life.arc")
  1       0       0       0
  1       1       1       0
  1       1       0       1
  1       0       0       0
  --------------------------------------------------------------------------------
  1       0       0       0
  0       0       1       0
  1       1       1       1
  1       0       1       1
  --------------------------------------------------------------------------------
  0       0       0       0
  0       0       1       1
  1       0       0       0
  0       0       0       0
  --------------------------------------------------------------------------------
  nil
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.

-----

5 points by adm 5946 days ago | link

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.

-----

3 points by fallintothis 5947 days ago | link | parent | on: Arc 3.1 (works on latest MzScheme)

I've updated my syntax highlighter & ftplugin with the new intrasymbol syntax, different/new Arc functions, and some other minor changes: http://www.vim.org/scripts/script.php?script_id=2720

This resolves part of the issue from before (http://bitbucket.org/fallintothis/arc-vim/issue/1/highlighti...). For the full changeset, see http://bitbucket.org/fallintothis/arc-vim/changesets/.

-----

More