Arc Forumnew | comments | leaders | submit | fallintothis's commentslogin
3 points by fallintothis 5570 days ago | link | parent | on: Care to share your arc project?

http://bitbucket.org/fallintothis

The projects there have been described in my corresponding posts about them: http://arclanguage.org/submitted?id=fallintothis.

There's also the community-wide repo dubbed "Anarki" for alterations to Arc: http://github.com/nex3/arc/.

A lot of posts here are about people's projects, so have a poke around (e.g., aw's stuff at http://awwx.ws/ or evanrmurphy's http://tryarc.org/).

-----

4 points by fallintothis 5570 days ago | link | parent | on: More documentation

To fill in the blanks of your not sures:

  allchars
  filechars
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.

For instance,

  (diskvar foo "some-file.txt")
is a macro that expands into

  (fromdisk foo "some-file.txt" nil readfile1 writefile)
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").

Similarly, disktable is defined thus:

  (mac disktable (var file)
    `(fromdisk ,var ,file (table) load-table save-table))
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.,

  arc> (system "ls | grep foo")
  nil
  arc> (diskvar foo "foo")
  nil
  arc> (system "ls | grep foo")
  nil
  arc> (= foo 8675309)
  8675309
  arc> (todisk foo)
  8675309
  arc> (system "ls | grep foo")
  foo
  nil
  arc> (do (system "cat foo") (prn))
  8675309
  nil
  arc> (todisk foo (+ foo 1))
  8675310
  arc> (do (system "cat foo") (prn))
  8675310
  nil
This way, we can have persistent on-disk variables.

  down
This is a backwards for.

  arc> (do (down i 5 1 (prn i))
           (prn "blast-off!"))
  5
  4
  3
  2
  1
  blast-off!
  "blast-off!"

  out
(out expr) evaluates expr, captures the output, and prints it. E.g.,

  arc> (= x 5)
  5
  arc> (out (++ x))
  ""
  arc> x
  6
  arc> (out (prn "hi there!"))
  hi there!
  "hi there!\n"
It was defined for news.arc in the function votelink:

  (out (gentag img src up-url* border 0 vspace 3 hspace 2))
If this didn't use out, the gentag macro would expand into

  (do (pr "<img")
      (aif up-url*
           (pr " src=\"" it #\"))
      (pr " border=0")
      (pr " vspace=3")
      (pr " hspace=2")
      (pr ">"))
thus evaluating up-url each time votelink is called. But out evaluates the macro expansion once, turning it into

  (pr "<img src=\"grayarrow.gif\" border=0 vspace=3 hspace=2>")
at compile-time. It's a bit of a fringe use.

  rand-key
Returns a random key of a hash-table.

  arc> (obj a 1 b 2 c 3)
  #hash((b . 2) (c . 3) (a . 1))
  arc> (rand-key that)
  c

  ratio
(ratio test xs) gives the ratio of elements of xs that satisfy test. E.g.,

  arc> (ratio even '(1 2 3 4 5))
  2/5
  arc> (ratio odd '(1 2 3 4 5))
  3/5
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.

-----

3 points by prestonbriggs 5570 days ago | link

Thanks for all of the clarifications.

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.

Preston

-----

2 points by evanrmurphy 5569 days ago | link

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
Had you known about this already?

[1] Or Java (http://github.com/conanite/rainbow, http://jarc.sourceforge.net/), or JavaScript (http://jonathan.tang.name/files/arclite/index.html), or Arc itself (http://arclanguage.org/item?id=11128). ^_^ I think someone did a port to Common Lisp not too long ago as well, but I can't find the link.

[2] http://github.com/nex3/arc/commits/master/help/arc.arc

-----

2 points by fallintothis 5568 days ago | link

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

-----

1 point by prestonbriggs 5569 days ago | link

I didn't know of it. Sounds great. Plus, I should be able to write a little program to grovel through his system and gen up a TeX document like mine.

Thanks for the pointer, Preston

-----

1 point by evanrmurphy 5570 days ago | link

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]

[1] http://arclanguage.org/item?id=11738

-----

1 point by prestonbriggs 5569 days ago | link

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.

Preston

-----

5 points by fallintothis 5570 days ago | link | parent | on: Named loop or continue keyword?

Looks good. That's what I would've done, if you hadn't beaten me to it. :)

Relevant arc.arc definitions:

  (mac point (name . body)
    (w/uniq (g p)
      `(ccc (fn (,g)
              (let ,name (fn ((o ,p)) (,g ,p))
                ,@body)))))

  (mac catch body
    `(point throw ,@body))
Thus,

  (catch
    (while t
      (throw 5)))
expands into

  (point throw
    (while t
      (throw 5)))
which expands (modulo gensyms) into

  (ccc (fn (current-continuation)
         (let throw (fn ((o return-val)) (current-continuation return-val))
           (while t
             (throw 5)))))
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.

  arc> (load "macdebug.arc") ; see http://www.arclanguage.org/item?id=11806
  nil
  arc> (macsteps '(while test
                    (point continue body1 body2 body3))
                 'show 'while 'point)
  Showing only: while, point

  Expression:

    (while test
      (point continue body1 body2 body3))

  Macro Expansion:

      (while test
        (point continue body1 body2 body3))

  ==> ((rfn gs2027 (gs2028)
         (when gs2028
           (point continue body1 body2 body3)
           (gs2027 test)))
       test)

  Expression:

    ((rfn gs2027 (gs2028)
       (when gs2028
         (point continue body1 body2 body3)
         (gs2027 test)))
     test)

  Macro Expansion:

      (point continue body1 body2 body3)

  ==> (ccc (fn (gs2029)
             (let continue (fn ((o gs2030)) (gs2029 gs2030))
               body1
               body2
               body3)))

  Expression:

    ((rfn gs2027 (gs2028)
       (when gs2028
         (ccc (fn (gs2029)
                (let continue (fn ((o gs2030)) (gs2029 gs2030))
                  body1
                  body2
                  body3)))
         (gs2027 test)))
     test)

  nil
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

  (mac esc-while (test . body)
    `(point break (while ,test (point continue ,@body))))
or, crafting a portmanteau à la whilet,

  (mac whilesc (test . body)
    `(point break (while ,test (point continue ,@body))))
Don't know if this actually made continuations any clearer, but there you go.

-----

4 points by fallintothis 5580 days ago | link | parent | on: rif

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

-----

1 point by evanrmurphy 5580 days ago | link

Good catch.

-----

2 points by fallintothis 5581 days ago | link | parent | on: Dot notation and doc in general

I have a write-up about the dot notation (which is part of a broader feature dubbed "ssyntax"), along with a tool that lets you see where/how you can use it: http://arclanguage.org/item?id=11179. Also, a breakdown of the precedence levels: http://arclanguage.org/item?id=11675.

-----

1 point by prestonbriggs 5581 days ago | link

Ah, cool tool. I like it.

Preston

-----

1 point by fallintothis 5583 days ago | link | parent | on: Try Arc->Javascript

I think js.arc or arc2js work well, for what it's worth.

-----

3 points by fallintothis 5583 days ago | link | parent | on: Try Arc->Javascript

I finally looked over your code a bit. Thoughts:

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

  (def imap (f xs)
    (when xs
      (if (acons xs)
          (cons (f (car xs)) (imap f (cdr xs)))
          (f xs))))

  (def on-qq (f expr)
    (list 'quasiquote
          ((afn (level x)
             (if (is level 0)
                  (f x)
                 (atom x)
                  x
                 (is (car x) 'quasiquote)
                  (list car.x (self (+ level 1) cadr.x))
                 (in (car x) 'unquote 'unquote-splicing)
                  (list car.x (self (- level 1) cadr.x))
                  (imap [self level _] x)))
           1 (cadr expr))))

  (def ssexpand-all (expr)
    (if (ssyntax expr)
        (let expanded (ssexpand expr)
          (if (is expanded expr)
              expr
              (ssexpand-all expanded)))
        (check expr
               atom
               (case (car expr)
                 quasiquote (on-qq ssexpand-all expr)
                 fn         `(fn ,(cadr expr) ,@(imap ssexpand-all (cddr expr)))
                 assign     `(assign ,(expr 1) ,(ssexpand-all (expr 2)))
                 quote      expr
                            (imap ssexpand-all expr)))))
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).

Good work so far!

-----

1 point by evanrmurphy 5583 days ago | link

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:

  (function(g3701){
    return g3701=(function(g3702){
      return (g3702?function(){bar;return g3701(foo);})['call'](this):nil);
    });
  })['call'](this,nil)(foo);
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.

Thanks for the valuable feedback.

-----

1 point by rocketnia 5583 days ago | link

(fallintothis) It's interesting because this project isn't just an Arc-to-Javascript compiler, but also a DSL for Javascript in Arc.

(evanrmurphy) I knew this was true but hadn't yet found a way to articulate it. Thanks for the good choice of words.

I was also impressed by that choice of words. XD

-

wouldn't try/catch be suitable as well?

That's what I would use. That way a continuation could be called from within a (do ...) boundary.

-----


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|
For braver people than me, you might check the source at http://github.com/plt/racket/blob/master/src/racket/src/read....

-----

2 points by fallintothis 5586 days ago | link | parent | on: Forum for basic questions?

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

To your second question, MzScheme was renamed to Racket; see http://racket-lang.org/new-name.html. Did you try ./racket instead of ./mzscheme?

-----

1 point by yayitswei 5586 days ago | link

Wow, thanks for your speedy response!

I did try ./racket too-- what's weird to me is that the files are there, and executable, but I can't run them. Have you seen this behavior before?

  wei@wei:/usr/plt/bin$ ls
  total 6.3M
  -rwxrwxr-x 1 root root  616 Jun  6 03:31 tex2page
  -rwxrwxr-x 1 root root  606 Jun  6 03:31 swindle
  -rwxrwxr-x 1 root root 2.0K Jun  6 03:31 slideshow
  -rwxrwxr-x 1 root root  624 Jun  6 03:31 slatex
  -rwxrwxr-x 1 root root  613 Jun  6 03:31 setup-plt
  -rwxrwxr-x 1 root root  614 Jun  6 03:31 scribble
  -rwxrwxr-x 1 root root  593 Jun  6 03:31 raco
  -rwxrwxr-x 1 root root 2.4M Jun  6 03:31 racket
  -rwxrwxr-x 1 root root  618 Jun  6 03:31 plt-web-server
  -rwxrwxr-x 1 root root  610 Jun  6 03:31 plt-r6rs
  -rwxrwxr-x 1 root root  610 Jun  6 03:31 plt-r5rs
  -rwxrwxr-x 1 root root  608 Jun  6 03:31 plt-help
  -rwxrwxr-x 1 root root 2.0K Jun  6 03:31 plt-games
  -rwxrwxr-x 1 root root  616 Jun  6 03:31 planet
  -rwxrwxr-x 1 root root  628 Jun  6 03:31 pdf-slatex
  -rwxrwxr-x 1 root root  625 Jun  6 03:31 mztext
  -rwxrwxr-x 1 root root  16K Jun  6 03:31 mzscheme
  -rwxrwxr-x 1 root root  623 Jun  6 03:31 mzpp
  -rwxrwxr-x 1 root root  615 Jun  6 03:31 mzc
  -rwxrwxr-x 1 root root 2.0K Jun  6 03:31 mred-text
  -rwxrwxr-x 1 root root  16K Jun  6 03:31 mred
  -rwxrwxr-x 1 root root 2.0K Jun  6 03:31 gracket-text
  -rwxrwxr-x 1 root root 3.9M Jun  6 03:31 gracket
  -rwxrwxr-x 1 root root 2.0K Jun  6 03:31 drscheme
  -rwxrwxr-x 1 root root 2.0K Jun  6 03:31 drracket
  wei@wei:/usr/plt/bin$ ./racket
  -bash: ./racket: No such file or directory
Thanks,

Wei

-----

3 points by evanrmurphy 5585 days ago | link

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.

-----

2 points by fallintothis 5585 days ago | link

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.

-----


That code could be a lot simpler. Here's my step-by-step rewrite of same-shape.

I space things out so I can read them.

  (def same-shape (lst1 lst2)
    (if (and (atom lst1) (atom lst2))
         t
        (or (atom lst1) (atom lst2))
         nil
         (if (and (empty lst1) (empty lst2))
              t
             (or (empty lst1) (empty lst2))
              nil
              (with (l1 (car lst1) l2 (car lst2) r1 (cdr lst1) r2 (cdr lst2))
                (if (and (atom l1) (atom l2))
                     (same-shape r1 r2)
                    (and (acons l1) (acons l2))
                     (and (same-shape l1 l2) (same-shape r1 r2))
                     nil)))))
With this spacing, the first thing that sticks out to me is that you have an if in the top-level if's "else" branch, when one flat if will do.

  (def same-shape (lst1 lst2)
    (if (and (atom lst1) (atom lst2))
         t
        (or (atom lst1) (atom lst2))
         nil
        (and (empty lst1) (empty lst2))
         t
        (or (empty lst1) (empty lst2))
         nil
         (with (l1 (car lst1) l2 (car lst2) r1 (cdr lst1) r2 (cdr lst2))
           (if (and (atom l1) (atom l2))
                (same-shape r1 r2)
               (and (acons l1) (acons l2))
                (and (same-shape l1 l2) (same-shape r1 r2))
                nil))))
Next, I see empty, which is defined in arc.arc as

  (def empty (seq)
    (or (no seq)
        (and (or (is (type seq) 'string) (is (type seq) 'table))
             (is (len seq) 0))))
Since we're talking about lists, you're really checking the no condition. In arc.arc, no is defined as

  (def no (x) (is x nil))
But

  arc> (atom nil)
  t
  arc> (acons nil)
  nil
So, the empty checks are already covered by atom.

  (def same-shape (lst1 lst2)
    (if (and (atom lst1) (atom lst2))
         t
        (or (atom lst1) (atom lst2))
         nil
         (with (l1 (car lst1) l2 (car lst2) r1 (cdr lst1) r2 (cdr lst2))
           (if (and (atom l1) (atom l2))
                (same-shape r1 r2)
               (and (acons l1) (acons l2))
                (and (same-shape l1 l2) (same-shape r1 r2))
                nil))))
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

  (if (and (atom lst1) (atom lst2))
       t
      (or (atom lst1) (atom lst2))
       nil
       'else)
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".

  (or (and (atom lst1) (atom lst2))
      (and (acons lst1)
           (acons lst2)
           'else))
So far, that's

  (def same-shape (lst1 lst2)
    (or (and (atom lst1) (atom lst2))
        (and (acons lst1)
             (acons lst2)
             (with (l1 (car lst1) l2 (car lst2) r1 (cdr lst1) r2 (cdr lst2))
               (if (and (atom l1) (atom l2)) 
                    (same-shape r1 r2)
                   (and (acons l1) (acons l2)) 
                    (and (same-shape l1 l2) (same-shape r1 r2))
                    nil)))))
That leaves the inner if to clean up. The nil at the end is redundant; removing gives us

  (def same-shape (lst1 lst2)
    (or (and (atom lst1) (atom lst2))
        (and (acons lst1)
             (acons lst2)
             (with (l1 (car lst1) l2 (car lst2) r1 (cdr lst1) r2 (cdr lst2))
               (if (and (atom l1) (atom l2)) 
                    (same-shape r1 r2)
                   (and (acons l1) (acons l2)) 
                    (and (same-shape l1 l2) (same-shape r1 r2)))))))
To simplify, we can expand the with variables and see what's happening.

  (if (and (atom (car lst1))
           (atom (car lst2)))
       (same-shape (cdr lst1) (cdr lst2))
      (and (acons (car lst1))
           (acons (car lst2)))
       (and (same-shape (car lst1) (car lst2))
            (same-shape (cdr lst1) (cdr lst2))))
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.

The final version is thus

  (def same-shape (lst1 lst2)
    (or (and (atom lst1) (atom lst2))
        (and (acons lst1)
             (acons lst2)
             (same-shape (car lst1) (car lst2))
             (same-shape (cdr lst1) (cdr lst2)))))
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.

  (def congruent (x y)
    (or (and (atom x) (atom y))
        (and (acons x)
             (acons y)
             (congruent (car x) (car y))
             (congruent (cdr x) (cdr y)))))
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.

  (def same-sig (x y)
    (or (and (atom x) (atom y))
        (and (acons x)
             (acons y)
             (if (and (var-arg (car x)) (var-arg (car y)))
                  (same-sig (cdr x) (cdr y))
                 (and (acons (car x)) (acons (car y)))
                  (and (same-shape (car x) (car y))
                       (same-sig (cdr x) (cdr y)))))))
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.

  (def optional (parm)
    (caris parm 'o))

  (def same-sig (x y)
    (or (and (atom x) (atom y))
        (and (acons x)
             (acons y)
             (if (and (optional (car x)) (optional (car y)))
                  (same-sig (cdr x) (cdr y))
                 (and (~optional (car x)) (~optional (car y)))
                  (and (same-sig (car x) (car y))
                       (same-sig (cdr x) (cdr y)))))))
The common pattern here annoys me, so I refactor a bit more. My final version is

  (def both (f x y) (and (f x) (f y)))

  (def optional (parm)
    (caris parm 'o))

  (def congruent-sigs (x y)
    (or (both atom x y)
        (and (both acons x y)
             (if (both optional (car x) (car y))
                  (congruent-sigs (cdr x) (cdr y))
                 (both ~optional (car x) (car y))
                  (and (congruent-sigs (car x) (car y))
                       (congruent-sigs (cdr x) (cdr y)))))))
Though you might consider defining it like

  (def congruent-sigs (f g)
    ((afn (x y)
       ...)
     (sig f) (sig g)))
to avoid calling sig manually.

-----

2 points by rocketnia 5599 days ago | link

I'm not sure if this makes the code any more readable, but, you know... http://en.wikipedia.org/wiki/Rule_of_three_(programming)

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

-----

3 points by fallintothis 5598 days ago | link

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

-----

2 points by rocketnia 5598 days ago | link

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)))
Thanks for your insight. ^_^

-----

More