allchars reads all of the characters from a stream, returning a string of those characters. E.g.,
(w/infile file "foo.txt"
(allchars file))
gives you the contents of the file "foo.txt". The above example is the actual definition of filechars: (filechars file-name) gives you the contents of the file file-name.
atend
Tests if an index i is at the end of a sequence s (or beyond the end, technically; the definition is literally (> i (- (len s) 2))). Stupid example:
(let seq '(a b c)
(forlen i seq
(prn (seq i))
(when (atend i seq)
(prn "done!"))))
prints
a
b
c
done!
nil
You can see not-stupid examples in app.arc.
cache
defcache
defcache is like def, except the function defined is wrapped in cache. E.g.,
(= x 0)
(defcache foo 60
(++ x))
defines a function of 0 arguments, foo, which increments x. But the result is cached for 60 seconds -- it doesn't execute the body again until you call foo after the cache has expired. So,
arc> (foo)
1
arc> (foo) ; immediately after
1
arc> (foo) ; 60+ seconds later
2
and so on.
savers*
fromdisk
todisk
disktable
diskvar
The global variable is an implementation detail of fromdisk; it's a hash table of variable names to saving functions ("savers").
(fromdisk var file init load save) creates the variable var if it's not already bound; its value is stored on disk in the file file (if the file doesn't exist, var defaults to init); its value is read from the file via the load function; and its value is stored to the file via the save function.
When we want foo's value, we (readfile1 "some-file.txt"). If the file doesn't exist, foo is set to nil. When we store foo back to disk (using todisk), it's like doing (writefile foo "some-file.txt").
The saver functions are triggered by todisk. (todisk var) writes var to the disk using its saver function. (todisk var expr) updates var to the result of expr before using its saver function. E.g.,
If the sequence is empty, it returns 0 (so there's no chance of a divide-by-zero error).
togglemem
(togglemem x place (o test)) is like a destructive version of
(if (mem x place)
(rem x place)
(adjoin x place test))
For example,
arc> (= xs '(a b c))
(a b c)
arc> (togglemem 'b xs)
(a c)
arc> xs
(a c)
arc> (togglemem 'b xs)
(b a c)
arc> xs
(b a c)
writefile
(writefile val file) writes the value val to the file file. Note that val is not appended; if file exists, it's overwritten.
Phew! Documentation is a lot of work, and I didn't even do that much. :)
Not to belittle the value of documentation (you've certainly done a good job!), but the neat thing about all of these is that I didn't need to look up the prose: their definitions in arc.arc really are that simple. If you're ever stuck or confused about how something works, I encourage you to try looking at its source code. I mean, it's pretty easy to tell what ratio does by reading
(def ratio (test xs)
(if (empty xs)
0
(/ (count test xs) (len xs))))
An English description reads almost exactly like the source code. But I hope my English descriptions are of some help to your project anyways.
I didn't find it at all easy to learn what I wanted from the source code. I mean, imagine I'm sitting here trying to write my first program in Arc. I know I need a loop, say, but which one? I don't even know the names yet. So I search backwards and forwards through arc.arc (or was this one in ac.scm, I can't remember!), finding first one variety then another. And interpreting the meaning of the source when I'm still learning is slow going. Perhaps you've forgotten :-)
Searching the forum wasn't much better. Imagine searching for "cache". Short words are handy to type, but don't yield useful search terms.
So I decided to write things down where I could print them out in a single command, yielding a relatively compact description, and look them up conveniently.
I can definitely imagine better documentation easing the learning curve for new Arc programmers. Source code can be self-explanatory, but only after you've attained a certain base level of fluency with the language. Just as the axioms have to be defined in Racket [1] before Arc can work, they have to be defined in your head before it can make any sense. So I applaud your effort.
One resource that really helped me learn faster was rntz's help system, available in Anarki [2]. It's very convenient because it gives you access to documentation right at the repl:
arc> help.acons
[fn] (acons x)
Determines if `x' is a `cons' cell or list.
Unlike 'alist, this function will return nil if given an empty list
See also [[atom]] [[alist]] [[dotted]] [[isa]] [[cons]] [[list]]
nil
arc> help.alist
[fn] (alist x)
Return true if argument is a possibly empty list
Unlike 'acons, this function returns t when given an empty list
See also [[atom]] [[acons]] [[dotted]] [[isa]] [[cons]] [[list]]
nil
arc> help.afn
[mac] (afn parms . body)
Creates a function which calls itself with the name `self'.
See also [[fn]] [[rfn]] [[aif]] [[awhen]] [[aand]]
nil
Source code can be self-explanatory, but only after you've attained a certain base level of fluency with the language. Just as the axioms have to be defined in Racket before Arc can work, they have to be defined in your head before it can make any sense. So I applaud your effort.
Well put. My thoughts exactly. The suggestion wasn't necessarily to learn Arc by spending a couple days reading the source. I mean, that's what I did, but I was already familiar with Common Lisp. Just that once you reach a certain point (as Preston seems to have), it's not so daunting to say "hey, I wonder what filechars does?" and go read
(def filechars (name)
(w/infile s name (allchars s)))
The ambiguities of certain names in Arc really used to confuse me. acons is the cons predicate and alist the list predicate, but afn isn't the function predicate. Moreover, alist has nothing to do with alists (association lists).
Now that I have better command over the language, these things don't bother me so much. They have their own sense, and I increasingly appreciate how "externally exposed names [are] approximately hamming encoded." [1]
I don't mind short names. Indeed, one of my hopes in learning Arc is to get out from under the sheer bulk of the code I usually have to write to implement my ideas.
So throw serves to "point back" to the current continuation, and optionally continues with a return value. (This seems to be the behavior of a simple (ccc (throw) ...) without the let; I'm not sure why it's defined this way.) The above loop returns 5 -- the continuation captured by ccc is outside of the loop, so its action is to return whatever value we say the while expression returns and continue evaluating anything after it.
This way, Arc gives you a decorator for return values. You can use this pattern to decorate any portion of your loop, to give different effects. Outside of the loop, it's like a break. Inside of the loop, it's like a continue due to how while is expanded.
As you can see, the continuation is captured around the body, but the test properly resides outside of the body, so it'll still be executed even if you continue (because, again, the default continuation is to keep on chuggin' along).
You could have a while macro encapsulate these (similar to catch) so you don't have to write
(point break
(while test
(point continue
body)))
I've been trying to think of good names for such a loop. The most descriptive words are long (e.g., continuable-while, breakable-while), but we usually recognize "esc" as "escape", so it's a short yet descriptive option. Something like
Bug report: aif knows there will be no destructuring in the variable (its variable is it), so it needn't worry whether the variable is actually an expression in certain contexts. But if you pass a destructuring expression to rif (which uses the same logic), there will be problems.
arc> (rif (a b) (list 1 2) 'c 'd)
Error: "Function call on inappropriate object 1 (2)"
arc> (macex1 '(rif (a b) (list 1 2) 'c 'd))
(let (a b) (list 1 2) (if (a b) (quote c) (quote d)))
It needs to be rewritten more like iflet.
(mac rif (var expr . body)
(w/uniq gv
`(let ,gv ,expr
(if ,gv
(let ,var ,gv ,(car body))
,(if (cddr body)
`(rif ,var ,@(cdr body))
(cadr body))))))
arc> (rif (a b) (list 1 2) (+ a b) 'c)
3
arc> (rif (a b) nil (+ a b) (list 3 4) (- a b))
-1
arc> (rif (a b) nil (+ a b) nil (- a b) 'c)
c
- Harumph. Looking back, I notice my ssexpand-all doesn't strictly work right. Instead of considering the actual cases, I just special-cased quotes and quoted unquotes. Technically, these are wrong.
arc> (ssexpand-all '`(a:b:c d))
(quasiquote ((compose a b c) d))
arc> (ssexpand-all '(fn (x:y) x:y))
(fn ((compose x y)) (compose x y))
arc> (ssexpand-all '(assign x:y z))
(assign (compose x y) z)
ssexpand-all should have a similar shape to macex-all, though it's more complicated and probably doesn't help much.
Bah. It feels like I've written this code about a million times. Gross.
- I notice you wind up copy/pasting a lot of arc.arc definitions, particularly for macros. Couldn't it be more general? I.e., add a clause like
(isa (car s) 'mac) (js (macex1 s))
to js1. I figure I'd want any ol' macro to expand, just so I could reuse existing code. Plus, this gives you all those verbatim arc.arc definitions for free. But you'd still want js-macs, since there are certain macros that shouldn't "spill over". E.g., the non-verbatim arc.arc definitions need to be tweaked for Javascript, but shouldn't be globally overwritten. You could make js-macs take priority over regular macros by checking for them first in js1, and still reduce copy/paste work. Would anything break from this?
- Really, the point about macros expands into a broader one. My first inclination is to model the compiler on ac.scm; once that's working, you get arc.arc & libs.arc for free just by cranking the compiler over them. Keyword collisions could be mitigated as in ac.scm, which prepends underscores to variable names.
arc> x
Error: "reference to undefined identifier: _x"
But this "compiler-oriented" approach might not work well. There are the Javascript keywords that you want to use from Arc without them getting treated like variables (mainly the reserved words). It's interesting because this project isn't just an Arc-to-Javascript compiler, but also a DSL for Javascript in Arc. So there needs to be some sort of balance.
There are more "overlap-y" examples like while. It's implemented as a macro in arc.arc, but you'd probably want to compile it down to Javascript's while instead of tail-recursive function calls. Also, some features don't line up quite right, like first-class continuations. Though you could compile them into Javascript (that'd be cool!), a big use for them is simply
(catch
(while t
(throw)))
which in more idiomatic (and probably more efficient) Javascript would be a while/break. So it isn't just calls to something specific like while, but also entire patterns of code that overlap. In the limit, you have Arc compile to complex Javascript, but I don't know how well Javascript handles as a target language for some of the crazier features like continuations.
I'm curious if you've tried this approach at all and, if so, how it panned out (I've never tried anything like it myself, so I wouldn't know).
As I was going through copy-pasting and tweaking definitions from arc.arc, I thought several times that there should be a smart way to automate the process. (And it is true that a large portion of the defs and macs are verbatim.)
For a long time I didn't have js-mac and wasn't using js-def. Each macro got a handwritten js-<insert name> function and its own branch in js1's if; each function was handwritten in Javascript. [http://arclanguage.org/item?id=11918] I finally got smart enough to start using the partially bootstrapped compiler, and abstracting away the verbatim definitions should follow from that.
You could make js-macs take priority over regular macros by checking for them first in js1, and still reduce copy/paste work. Would anything break from this?
I do think that will be the way to do it, and it shouldn't break anything.
It's interesting because this project isn't just an Arc-to-Javascript compiler, but also a DSL for Javascript in Arc.
I knew this was true but hadn't yet found a way to articulate it. Thanks for the good choice of words.
There are more "overlap-y" examples like while. It's implemented as a macro in arc.arc, but you'd probably want to compile it down to Javascript's while instead of tail-recursive function calls.
Yes. Since resources are so precious in the browser and Javascript doesn't optimize tail calls, this will probably be an important refinement. As a bonus, it may make the compiled js more readable, since (while foo bar) would be generating something like while(foo){bar;}, rather than:
Also, some features don't line up quite right, like first-class continuations...
Your idea of compiling the common case to while/break sounds fine, but wouldn't try/catch be suitable as well? (I haven't made enough use of either first-class continuations or Javascript's try/catch to know yet.) There have been some cases I've dealt with already where the features didn't quite line up: rest/optional parms for functions, conses/lists vs. arrays and nil vs. false/null/undefined.
Not quite sure (I suspect it's a bug), but it seems like it has to do with the implementation of make-readtable (which brackets.scm uses).
$ mzscheme
Welcome to MzScheme v4.2.1 [3m], Copyright (c) 2004-2009 PLT Scheme Inc.
> (parameterize ((current-readtable #f)) (read))
x`y ; read in as two items
x
> y
> (parameterize ((current-readtable (make-readtable #f))) (read))
x`y ; read in as one symbol
|x`y|
This is the right place to ask basic questions -- don't be scared, we're glad to answer. :) E.g., I've answered the question about asterisks before: http://arclanguage.org/item?id=11104. (The short answer is that it's a naming convention for global variables.)
I'm not sure, but maybe you need to do something like
chmod +x racket
before it will work as an executable? You might also try the users@racket-lang.org mailing list, #racket on freenode.net, or one of the other resources at http://racket-lang.org/community.html to get input on this problem.
That is weird. Do you remember the options you gave the installer? Can you reproduce this problem on a fresh install with those same options (the installer probably created a racket-uninstall script)? If the installer created them (try which racket), do the /usr/bin ones work? I really don't know what's up. I'm still running mzscheme v4.2.1, so I'm not sure if/why Racket would create all those binaries with the old names, either -- mzscheme, drscheme, etc.
Next, I see t and nil floating around in an if -- so most of it could probably be done with and/or instead of if, which might make the code clearer. Let's check with some boolean simplification. The top-level if goes
Here's a truth table based on (atom lst1) and (atom lst2).
(atom lst1) | (atom lst2) || (and ...) | (or ...) | (if ...)
nil | nil || nil | nil | 'else
nil | t || nil | t | nil
t | nil || nil | t | nil
t | t || t | t | t
Notice that the if is equivalent to the and, except for the "else". So, whenever the and returns t, we want to return t; i.e., if should be replaced by or. But
(or (and (atom lst1) (atom lst2))
'else) ; wrong
isn't quite right. When the and is nil, we only want to do the "else" if both (atom lst1) and (atom lst2) are false. Note that in arc.arc we have
(def atom (x) (no (acons x)))
So, (acons x) is also the same as (no (atom x)). Therefore, we can guard the "else" by checking that both (acons lst1) and (acons lst2) are true. This communicates the intent more clearly for whatever we have in the "else".
So, the if decides whether to recurse on the cars of the lists. But notice that whenever
(and (atom (car lst1))
(atom (car lst2)))
we know that
(same-shape (car lst1) (car lst2))
by the definition of same-shape, because the first clause of the or is that very atom check!
So, we don't need this if. We can handle it all by recursion. This also means we can get rid of the with, because we don't need to use car/cdr expressions more than once.
I detailed the rewrite for learning's sake, but we could've saved a lot of time noticing that it's really similar to Arc's iso:
(def iso (x y)
(or (is x y)
(and (acons x)
(acons y)
(iso (car x) (car y))
(iso (cdr x) (cdr y)))))
Only instead of is, we're checking that both of the items are atoms. This also suggests a different name: iso comes from the word "isomorphism", so maybe a good name for same-shape is congruent. But I like same-shape, too. Either works.
We can apply a lot of these lessons to same-sig (or maybe congruent-sigs, since we're checking for shape, not equality, and same-sig-shape is awkward). However, we don't have quite the same nice recursive properties that same-shape does.
But with this rewrite, we can see some potential bugs.
First, the var-arg clause checks the cdrs if the cars both take either one of two forms. So,
arc> (same-sig '(x y) '((o x) (o y)))
t
This might be desired -- both parameter lists are "compatible" if we supply the optional arguments. But I reckon with a name like same-sig, it's a bug.
Second, you use same-shape to check if two destructuring parameters are congruent. But same-shape doesn't take into account optional parameters, which are perfectly valid in destructuring lists. E.g.,
arc> (let (x y (o z)) '(a b) (prs x y z) (prn))
a b nil
nil
arc> (let (x y (o z)) '(a b c) (prs x y z) (prn))
a b c
nil
So same-sig gives
arc> (same-sig '(a (b (o c))) '(a (b (c d))))
t
which is surely a bug.
To address the first (potential) bug, instead of using var-arg we can just check for optional parameters. For the second, just use same-sig instead of same-shape.
; Short for "if both or neither and," 'ibona checks 'x and 'y against
; 'test (which is sent through 'testify). If one is truthy and the
; other is falsy, this results in nil. If they're both falsy, this
; results in an 'and of the 'ifneither and 'eitherway expressions. If
; they're both truthy, this results in an 'and of the 'eitherway
; expressions if they exist, and it propagates the result of calling
; the test on 'y if they don't.
(mac ibona (test x y ifneither . eitherway)
`(fn-ibona ,test ,x ,y
(fn () ,ifneither)
,(when eitherway `(fn () (and ,@eitherway))))))
(def fn-ibona (test x y ifneither (o eitherway))
(zap testify test)
(if do.test.x (aand do.test.y
(if eitherway (do.eitherway) it))
do.test.y (aand (ifneither)
(if eitherway (do.eitherway) it))))
(def congruent (x y)
(ibona atom x y
(and (congruent car.x car.y)
(congruent cdr.x cdr.y))))
(def congruent-sigs (x y)
(ibona atom x y
(ibona optional car.x car.y
(congruent-sigs car.x car.y)
(congruent-sigs cdr.x cdr.y))))
I've found myself doing...
(if atom.x atom.y
atom.y nil
...)
...a couple of times myself, so I wouldn't be surprised if (some variation of) 'ibona were surprisingly useful. :-p
EDIT: Hmm, this doesn't abbreviate as much, but its implementation and description are much more concise.
; This is similar to 'whenlet, but for binary if-and-only-if (i.e.
; xnor).
(mac xnor2let (var x y . body)
`(fn-xnor2let ,x ,y (fn (,var) ,@body)))
(def fn-xnor2let (x y body)
(only.body:.y:if x idfn no))
(def congruent (x y)
(xnor2let it atom.x atom.y
(or it
(and (congruent car.x car.y)
(congruent cdr.x cdr.y)))))
(def congruent-sigs (x y)
(xnor2let it atom.x atom.y
(or it
(xnor2let it (optional car.x) (optional car.y)
(and (or it (congruent-sigs car.x car.y))
(congruent-sigs cdr.x cdr.y))))))
Hmm, interesting. If I were opting for another control structure (which I probably wouldn't in this case, but hypothetically), I think I'd make your ibona closer to this (not tested):
(mac unless-both (test x y neither)
(w/uniq (f fx fy)
`(withs (,f (testify ,test) ,fx (,f ,x) ,fy (,f ,y))
(or (and ,fx ,fy)
(and (no ,fx)
(no ,fy)
,neither)))))
(def congruent (x y)
(unless-both atom x y
(and (congruent (car x) (car y))
(congruent (cdr x) (cdr y)))))
(def congruent-sigs (x y)
(unless-both atom x y
(and (unless-both optional (car x) (car y)
(congruent-sigs (car x) (car y)))
(congruent-sigs (cdr x) (cdr y)))))
I'm not sure if there's a better name than unless-both. Also, you could conceivably make neither a rest arg and just splice it into the and so you don't have to do (unless-both atom x y (and ...)). I just think it reads better with the explicit and. (Of course, adding a macro here is probably overkill anyway. Fun, though!)
Hmm... my code originally looked a lot like that (except for naming and the implementation of 'unless-both). Then I edited my post quite a bit because I thought there was a problem with the (and (unless-both ...) (congruent-sigs ...)) expression. (I can't remember what it was now, and I think I was mistaken.)
So I added the 'either-way parameter, and then I moved the logic into a function to make the evaluation order more to my liking--no calling the test on 'x before 'y is evaluated--and it got to the complicated state it's in now. In fact, I see some bugs now; the place where it says "do.test.y (aand (ifneither)" should be "do.test.y nil (aand (do.ifneither)".
For whatever it's worth, here's 'ibona again, with your much better name, and without 'eitherway. The only thing that's really different from your version is the argument evaluation timing.
(mac unless-both (test x y ifneither)
`(fn-unless-both ,test ,x ,y (fn () ,ifneither)))
(def fn-unless-both (test x y ifneither)
(zap testify test)
(if do.test.x do.test.y
do.test.y nil
(do.ifneither)))