I don't feel any rush in producing new versions, just as I didn't feel any rush in releasing the first one. Lisp hackers have waited 50 years for a good implementation. That didn't kill Lisp. Hackers used to this idea aren't going to panic because Arc hasn't been updated for n months. And the ones who feel that an actively growing source tree is more important than the underlying language were in the wrong place to begin with.
The next release will have more improvements to news.arc than the underlying language, because that's what I've been working on most lately. But I'm going to be focusing more on the language soon.
Personally, I think the only thing that went wrong in the history of Arc is that pg didn't correct people's misunderstandings about what it was, and what it was trying to achieve. A 100-year language is not going to be usable tomorrow, and a language designed by the optimal number of people (one) is not going to accept many patches!
If people want a practical language to use now (and there's nothing wrong with that!) then they should go ahead and build one. I'm happy to join any project that anyone may have for developing an Arc-like language. I might even start one myself (why not? It'll be fun!) if I have the time.
A language doesn't survive in its implementations, but in its ideas. Modern languages (especially Python/Ruby, but even C# and Java) are becoming Lisp. Dynamic typing, late binding, functional programming, closures, generic functions, MOP... the 'popular press' laughed at them, but now these are the hot topics in language design. Those ad hoc, informally-specified, bug-ridden, slow implementations of half of Common Lisp are getting better every year. Common Lisp and Scheme will die, but in 10 years everyone will be using Lisp, and they won't even know it.
The same goes for Arc. Clojure already uses some ideas from Arc (e.g. fewer parens in conditionals). If the ideas are good (and I think they are) then they'll spread. If people want to build new languages (e.g. Arc3F) that use them, then that spreads them further, That, I think, is the true future of Arc, as a source of eternal ideas.
1. The time scale. I don't want to make what people think they want right now. Following that recipe earlier would have got me Perl, which lost its lead to the language I would have gotten a little later, Python, which lost its lead to the language I would have gotten a little later, Ruby, which... See the pattern?
2. The audience. "Make something people want" is a recipe for companies. Their goal is to make a lot of money, which means aiming for a wide audience. That shouldn't necessarily be one's goal in every kind of work. It's ok to want to be Jane Austen instead of Perez Hilton, even though Perez Hilton is what most people want.
Oh honestly. Perl had the popularity Arc could have had. Then Python did, then Ruby. The fact that the "winner" keeps changing shows that it doesn't matter which one you beat.
I've long since stopped worrying about other languages. If they're genuinely better, they deserve to win. And if they merely have "momentum," they'll ultimately be superseded, just like every other language du jour before them.
x!y!z is (x 'y 'z) rather than ((x 'y) 'z); likewise x.y.z. While perhaps not a bug per se, this is clearly undesirable (at some point, adding more infix dots is less clear than just parenthesising, so x!y!z for (x 'y 'z) is not very useful, but chaining lookups is common and is much nicer to read as x!y!z than ((x 'y) 'z)) and should be fairly simple to fix.
()() ()()
# # # #
__#_#_#_#__
{_` ` ` ` `_}
_{_._._._._._}_
{_ H A P P Y _}
_{_._._._._._._._}_
{_ B I R T H D A Y _}
.---{_._._._._._._._._._}---.
( `"""""""""""""""""""""` )
`~~~~~~~~~~~~~~~~~~~~~~~~~~~`
The first three are right. As for 4, it's not so much that I prefer less communication as that I sometimes may be too busy to visit the site.
I think the thing people may not understand is that I can only work on Arc intermittently. In the spring as well as a YC cycle we had startup school and I got married. The next day the summer YC cycle started. During the summer I was busy with a larger than usual number of startups. As soon as it was over I went on honeymoon, during which I barely touched a computer. I got back to the US just before applications for the winter cycle were due. I've been reading them all the past week, and I still have over 100 left. (I probably wouldn't even be writing this if I weren't desperate for ways to procrastinate.) Then we have to move YC and ourselves to the west coast, then do interviews. I'm hoping that I'll be free to work on Arc in mid or late November, but I don't want to promise anything.
So it's not so much that I don't want to be held to a schedule as that I couldn't do things to a schedule even if I wanted to.
I'm hoping that ultimately doing so many things will make them all turn out better. But it does mean I can't do any of them more than part time.
In answer to your specific questions: I don't think I can commit to an upper bound for new releases, because the factors that determine whether or not I can work on Arc aren't cyclic. Yes, I'll incorporate bug fixes, the next time I get to work on the language, which I hope will be late this year. At the moment the most valuable things people interested in Arc could do are (a) find bugs in the current implementation, (b) think about the core language and specifically what new operators, if they existed, would make existing Arc programs like news.arc significantly smaller in tokens, and (c) try using Arc to write different kinds of applications and report what happens.
How are we supposed to look at Arc then? The problem doesn't lay only in the fact that no source has been written, but in the fact that who has full control over Arc doesn't discuss about the language with the community. Is Arc just a one-man work and arclanguage.org just a way to show the world this work from time to time?
It's ridiculous that to bring you back in the forum we needed such a harsh post.
What do you think about the history of Arc so far?
From my perspective, it appears to be a history of different expectations.
On the one hand, it seems that pg:
1. is taking a long term view of the language
2. is focusing on core language issues
3. doesn't want to be held to a schedule
4. prefers less communication with the community
whereas, it seems at least part of the community wants:
1. to see *signs of life*
2. useful libraries
3. to know the plan (or whether there is one)
4. more interaction from pg (communication, acceptance of bug fixes, etc.)
As with many misunderstandings, better communication might go along way in helping to set proper expectations.
For example, I have some questions for pg that might be shared by others in the community:
1) What is a reasonable upper bound for producing new releases of Arc - 6 months, 3 years, ... ?
2) Will you accept bug fixes for the language in the future? If so, approximately when?
3) Besides the profiler you mentioned, what contributions would you like from the community? Do you want any contributions from the community?
I think some of the disappointment that is felt by some in the community is due in part to the potential we see in the language. Yes, we know you're taking a long term view, but when we saw the potential to use Arc in place of current tools, we hoped we could use it soon. Obviously it's usable now, but that's a relative term.
Personally I'd volunteer to take the lead, really fork Arc, and go on with Arc-F. Unfortunately, I'm not at all experienced in actually launching and maintaining a website. Worse, Arc-F can't host news.arc (or a ported version) yet; I'm somewhat redesigning the Arc server because of the lousy and inconsistent design, but I'm kinda stuck on how to handle the continuation server functions bit, which strike me as the dirtiest bits of the Arc Server.
The last alternative - let Arc die - is something we should seriously consider though. Clojure is gaining the popularity that Arc could have had. Like Arc it's a redesigned Lisp (fn and new ssyntax, anyone?); unlike Arc, it wasn't hyped very much for >6 years, its maintainer is still visibly leading the language, and it runs on the JVM. The only advantage of Arc that I see is that it supports both imperative and functional paradigms - and some might argue that this isn't actually an advantage.
Letting Arc die would hurt me a lot, though. Other than the original writers of Arc, I can probably reasonably claim to have made the largest contribution to Anarki (many of the docstrings in the Anarki arc.arc, for example, were by me).
There's no preplanned development schedule. When there's an essay I want to write, I write it; when I want to work on Arc, I work on it-- and when there's a significant amount of new stuff, I make a new release.
As cchooper observed, macros are expanded at compile time, not runtime. This means that they cannot see lexical variables like k. (Since x was a global variable, it could be seen at compile time, thus making your first example work.) In order to look at the lexical environment, you need an ordinary function:
(def assign (name expr)
(eval `(= ,name ',expr)))
The reason you don't need a macro here is that you aren't actually suppressing evaluation, which is what macros do; you want the values of name and expr. Thus, assign should be a function, which runs at runtime. Also note that expr needs to be quoted, or you attempt to evaluate it, which fails if it is a symbol.
The one thing that might trip you up with this function is that it (like =) will not create lexical variables, but only globals, unless lexicals of the names you are about to assign are already declared.
Also, a formatting tip: to get text to appear as code, surround it by blank lines and indent it by two spaces.
That happened to me early on, because earlier versions of Arc were written in CL. Within a month or so I found some CL operators intolerably verbose. And yet I'd been using CL for 15 years and had never had a problem with them. That was an interesting data point about how much one gets used to the language one uses. So much so that external constraints (e.g. counting tokens) are needed to force one to come up with ideas for improvements.
Great question; I'm looking forward to other people's answers.
"When one writes a new tool it's usually (hopefully) to satisfy a pretty frustrating need."
Building a programming language is a pretty natural itch. For a certain kind of programmer, at least. I'll give you some reasoning and rationalizations for wart, without any certainty that I can separate the two.
I've been vaguely aware of lisp almost since I was exposed to computers, back in 1996. But in India algol-style languages reigned. Basic, fortran, cobol, pascal, C/C++. A little behind the times. I took a lisp course when I moved to the US back in '99 and I could program little things[1] in common lisp after that. But I'd still stare at things like the recursive definition for reverse and wonder precisely what it was that made it so different from what I'd write in C. I periodically go back to that question, but more on that further down.
After programming mostly in C for 10 years, burning out, recovering, the usual coming-of-age stuff, I found myself in the bay area doing rails and the startup scene for a couple of years ('07-'10). I'd been interested in social software and recommendation systems since Reddit got started. I even signed an NDA with the Reddit guys to get at their code and try to build a recommendation system for them. But the lawyers took too damn long and I'd lost interest by the time they sent it to me (sometime '07). But around '08 I started thinking about recommendation systems again. Arc was out by this point[2], and it acted as a great gateway drug. Here, finally, was a lisp that helped filter out all the cruft and let me focus on how lisp was different from C, and that was ergonomic enough that I had to work to stop programming in it. At some point I looked at the logs generated by news.arc, and noticed that they were just s-exprs that could be read back in if I so desired. Hmm, crazy idea: a recommendation system that can see the recent actions in a session to inform recommendation decisions. The easiest way to do that would be to stick with lisp.
So I left rails behind and started a fresh (and my final, so far) startup in arc, to build a feedreader that didn't require understanding RSS. I never did do much with the idea of adapting to the current session, because I could never get to a point where I knew enough about a user to make any intelligent decisions. Providing a bootstrap experience that kept them coming back long enough turned out to be an eternally unsolved problem. I found out the importance of UX and how much I sucked at it. A year later the project was dead and I was working at Google. But the interest in lisp stuck, and I continued doing stuff with it, noodling on a series of little questions about its design, why it did things this way and not that. Sometimes I learned the hard way something people knew back in the '60s. Occasionally I discovered something new[3].
But I was still dissatisfied. One of the things that had bothered me all through this period was webservers and memory leaks. No matter what platform I used -- rails, python, racket, common lisp -- it seemed any non-trivial website would periodically run out of memory and die. Everywhere I looked, people dealt with this in a brutal way: by periodically restarting their servers and clearing the slate. Ugly, and it seemed to point at an endemic problem: languages spent tons of effort tweaking their garbage collectors, but programmers would mess up in some subtle way and accidentally prevent stale objects from being collected. Often this was because of interactions between multiple projects. Often there were no tools for the low-level debugging required. Or if there was, you needed to learn too much about the high-level internals of the stack you were relying on.
There were other dissatisfactions. Common lisp was a lisp-2, and its APIs were ancient and baroque[4]. I could sense that I could get used to them, but the mind rebelled at polluting my brain with all those warts. Racket was more modern and a lisp-1, but it gave up a lot of the dynamism of lisp with its phase-separation rules, hygienic macros and module system. Both had oodles of documentation, but common lisp's docs were poor and examples often wouldn't run. It had a 'standard' constraining creativity and unionizing bad APIs with lots of detailed rules, but the standard hadn't been updated in years so you were often doing stuff out of its ambit anyway (I still don't understand its condition system). It was the worst of both worlds. Racket has a much nicer documentation system, and yet it was too overwhelming for reasons I still struggle to pinpoint[5]. At some point, it seemed, the documentation was complex enough to ask, why can't I just deal with the codebase directly rather than some interface to it? How can I fork racket and throw out hygienic macros until I gain the wisdom of their need?
So eventually, after leaving arc, after trying to build arc out of common lisp macros[6], after some time agonizing over just switching to Factor[7], I bit the bullet, threw out all the dependencies, and returned to my roots in C. My goals were to create a dynamic lisp-1 with non-hygienic macros where I could define code in any order[8], and to do so with as little code as possible, in such a way that others could come after me and query the design space I had explored, asking what-if questions: what if I want to turn off this feature? Why do we implement this like so? The goal was never to build a 'real' language the way the world thinks about it. I wanted to see how far I could push a design without any black-box interfaces, where programmers had to understand the entire stack, and therefore where the entire stack had to be simple enough to fit in a single head[9], one that encouraged the combination of high- and low-level fluency necessary to track down problems like memory leaks, one where the documentation was intertwined inextricably with the implementation, so it couldn't go out of date and it could be queried interactively[10]. One that didn't spend thousands of lines of code trying to 'optimize' arbitrary code, but provided programmers the tools to measure their applications and attack bottlenecks for themselves without compromising readability, the way 'macros' provide a la carte mechanisms for reducing our own boilerplate. That's The Lisp Way.
What I have so far is a far cry from this goal. Sometimes I worry that I'm just playing in the shallows where it's fun[11] and avoiding the scary abyss. Programming languages are fun, but if I can stay disciplined wart will be more than a language. And nobody will have to ever rediscover why something is built just like so.
[5] Many of my initial annoyances were bugs in arc at the time (http://arclanguage.org/item?id=13616), or a result of the legacy mzscheme language (since fixed by Arc/Nu). Would I have embarked on this if the timing had been just a little different?
[10] I don't want to require new programmers to understand every last line of code before they can use a program. I want the program to reward curiosity, so that newcomers are able to drill down into the right bits when they run into problems, to answer questions for themselves without needing to find an expert to talk to (http://alistair.cockburn.us/ASD+book+extract%3A+%22Naur,+Ehn...), and to iteratively try tweaking the code with instant feedback about regressions they introduce.
Running on the current version of mz would be very useful. It's really not fun to have to use a local version when you're used to running on software from a distribution.
Sure, I can share some things I've learned. The caveat of course is that I am no elite hacker, so some of this is going to be obvious or not really correct...
- macros: In news.arc, they are all over the place. So I tried to code in a similar style, but it turns out that if something can be done via a macro vs being done with a function, then it is more natural for me to use a function. I work a lot with the REPL, and it turns out to be really annoying to change a macro, and then I have to track down all the places that use that macro, and "refresh" them to use the new macro. Especially with macros that call macros that call macros, and I change the macro at the end of the chain. Whereas with a function, I just have to paste in the new function and it takes effect right away.
- news.arc stores posts in a hash table. I started off modeling ballerinc that way. But as time goes on, I am starting to move things over to plain old lists. Basically, working with a hash table pushes me into code that uses side effects to get things done, like using each. Whereas with lists, the code is usually cleaner and shorter.
- repl: using the repl on the running web server is pretty amazing. Already I have pushed changes directly to the live site. For ballerinc restarting the server takes about 10 minutes, so this has been a life saver. I don't worry about "releases" at all anymore - just make the feature, test it, and deploy it.
- the codebase is about 386 LOC. For the past week I've been adding features and doing code cleanup, and the LOC keeps going down.
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.
If I were you I'd look at it from the Lisp tradition: as part of a 50 year history with many dialects, rather than as an urgent effort to manage one particular implementation consisting of a huge pile of libraries on top of a badly designed language core.
People who want languages like that already have plenty of options, even Lisp dialects.
I've got to say I agree with pg on this. It's more important that Arc turn out well in the long run than in the short run. Also, although Arc isn't moving very fast, I have to say that the more I actually program in it, the more I appreciate its design. I still find myself missing the lack of a proper library/module/namespace system, and of course given that some actual libraries would be useful, but that's about it for truly major issues.
That said, I do think pg could do with talking to the community a little more, and vice-versa.