Arc Forumnew | comments | leaders | submitlogin
3 points by rocketnia 5093 days ago | link | parent | on: Revisiting + vs join

"Arc uses ar-nil-terminate and ac-niltree to do the Arc->Racket->Arc conversion. ar-nil-terminate does a shallow copy, but ac-niltree does a deep copy.

However, the Arc compiler could be changed so that + uses a shallow version of ac-niltree."

That's the essence of the bug, as I see it. This fix is much shallower than the other fixes discussed, so this fix would make the most sense in a minimally updated variant of pg's Arc.

Should Anarki go for shallow or deep improvement? I've advocated shallow in the past, but now I'm thinking Arc oughta follow through on its promise to "break all your code," which of course means the entire network of hacks, libraries, help resources, and alternate language implementations we've built up so far. It would be nice to see Anarki become a fork of Arc/Nu and undergo deep improvements, without losing the Arc flavor.


That's exactly what I mean! Thanks!

Hmm, I'm losing track of my point. I prefer writing code so it models all errors explicitly, so I end up with that kind of verbosity in JavaScript too. I only get code like foo(bar(qux(x))) when I'm using lots of function calls that have no errors (or whose arguments can be errors).

EDIT: My use of the term "error code" came about because of this subthread on LtU: http://lambda-the-ultimate.org/node/4606#comment-73197. I probably didn't give the term enough context to have a clear meaning.


Yes, and rocketnia's point is that this error code checking forces your code into something like SSA or three address code:

http://en.wikipedia.org/wiki/Static_single_assignment_form

http://en.wikipedia.org/wiki/Three_address_code

In other words, you usually won't see this in C, right?

  foo(bar(qux(1, 2, 3)))
Instead, you'd write it more like this:

  x = qux(1, 2, 3)
  if (x == ERR) {
    ...
  }

  x = bar(x)
  if (x == ERR) {
    ...
  }

  x = foo(x)
  if (x == ERR) {
    ...
  }
Because you don't have nested expressions, there's only a single way to indent the code. But in languages like JavaScript, method chaining and nested function calls mean that there's now multiple ways to indent the same code.

Thus, C's syntax isn't actually more uniform than Lisp, it only seems that way because of C's way of handling errors.

4 points by Pauan 5093 days ago | link | parent | on: Revisiting + vs join

akkartik: "I finally understood Pauan's diagnosis, that it was arc copying nil vs () terminated lists. I've been against that 'feature' for a long time."

Yes, but Arc/Nu manages to implement that Arc feature without conversion, so I don't consider it a problem with the feature, but a problem with the implementation.

---

akkartik: "+ and join on a single arg is just the identity function, right?"

Yes, but Arc does the conversion even if you pass in a single argument. So another possible solution to the problem is to change Arc to return the first argument unchanged when passed in a single argument.

---

zck: "If you call (+ '(((((((1))))))) '(((((((2)))))))) , you shouldn't have to change fifteen conses, just one or two."

Yes, you shouldn't have to. Arc uses ar-nil-terminate and ac-niltree to do the Arc->Racket->Arc conversion. ar-nil-terminate does a shallow copy, but ac-niltree does a deep copy. Simply changing ac-niltree wouldn't work, because I'm pretty sure other stuff depends on the deep copy.

However, the Arc compiler could be changed so that + uses a shallow version of ac-niltree. That's an interesting idea: how much stuff in the Arc compiler is currently using the deep ac-niltree when it could instead use a shallow ac-niltree?


I'm not sure what you mean by error codes. C doesn't have built-in support for things like exceptions.

Method-chaining doesn't come into play, because, technically, there is no such thing as methods. Just regular functions.

Errors are typically indicated in the return value of some function. Error checking, then, is an if block following the function call.

2 points by zck 5093 days ago | link | parent | on: Revisiting + vs join

Yes, the outer conses all have to change, except for the last one. But the inner conses don't, and they are changed right now. That, specifically, is my problem with '+ . If you call (+ '(((((((1))))))) '(((((((2)))))))) , you shouldn't have to change fifteen conses, just one or two.
1 point by akkartik 5094 days ago | link | parent | on: Revisiting + vs join

Sorry, I think we've both managed to confuse each other. + and join on a single arg is just the identity function, right? Aren't these the cases we care about?

  arc> (+ '(1 2) '(3 4))
  (1 2 3 4)
My argument was that in the presence of mutable conses all the conses would have to change. After your response I'm correcting that to "all the outer conses (except in the last arg)".

Does this seem right? I was reading your previous comment without one wrapping set of parens.

2 points by zck 5094 days ago | link | parent | on: Revisiting + vs join

That's neither '+ nor 'join :

  arc> (+ '((1) (2)))
  ((1) (2))
  arc> (join '((1) (2)))
  ((1) (2))
That's flatten:

  arc> (flat '((1) (2)))
  (1 2)
1 point by akkartik 5094 days ago | link | parent | on: Revisiting + vs join

But the result should be:

  ( 1 . , )                   
        `-------> ( 2 . nil)
So the first of your element conses has to change too.

But you are right that not all the conses need to change. I finally understood Pauan's diagnosis, that it was arc copying nil vs () terminated lists. I've been against that 'feature' for a long time. My private fork just stays with () terminated lists: http://github.com/akkartik/arc

3 points by zck 5094 days ago | link | parent | on: Revisiting + vs join

If the language used immutable conses, it still shouldn't have to change every cons. The issue here arises not because the list you pass in has different conses than the list returned, but because the elements of the list you pass in have different conses than the elements of the list returned.

Let's say we call (+ '((1) (2))). I'll draw the list this way:

  ( , . , )                   
    |   `-------> ( , . nil)  
    |               |         
    |               |         
    |               |         
    v               v         
   (1 . nil)       (2 . nil)
To my brain, there are two "kinds" of conses in the list: those that make up the top-level list itself, and those that make up the elements.

  top-level  ( , . , )                   
    conses     |   `-------> ( , . nil)  
               |               |         
               |               |         
               |               |         
   element     v               v         
    conses    (1 . nil)       (2 . nil)

And here's what I think should happen in the call to +

  these conses  ( , . , )
  can change      |   `-------> ( , . nil)
                  |               |
                  |               |
                  |               |
  these conses    v               v
  shouldn't      (1 . nil)       (2 . nil)
Similarly, if you call

  (+ (list (obj 1 2)))
Would you expect the hash table to have its elements copied into a new hash table? No, the same hashtable should be shared between the calling list and the returned list, even if the list itself has different conses. And indeed, this is the case (thanks again, evanrmurphy, for tryarc):

  arc> (def check-cars-obj (join-fun)
              (let x (list (obj 1 2))
                   (is (car (join-fun x))
                       (car x))))
  #<procedure: check-cars-obj>
  arc> (check-cars-obj +)
  t

"The nice thing about indenting in C is simply that it falls out naturally, so you don't have to think about it."

In my experience with Java, Groovy, and JavaScript, the indentation gets just as idiosyncratic when it comes to nested function call expressions, long infix expressions, and method chaining.

I've rarely used C, but does its error code idiom obscure this drawback? I imagine error codes force programmers to put many intermediate results into separate variables, whether they like it that way or not.


See above reply to Pauan.

There are other styles, but the differences aren't all that significant. I like K&R.


The nice thing about indenting in C is simply that it falls out naturally, so you don't have to think about it.

I'm not proposing to turn Lisp into C. The formless nature of Lisp is essential to what enables it to do things you can't do in C.


After about a week of work, I got a JS version of Nulan working well enough that I'm willing to show it off. For a taste of how it looks, check out NULAN.macros:

https://github.com/Pauan/nulan/blob/8353af71c33d63960d06579e...

Some more things you can try at the REPL:

  # function and infix syntax works
  ((-> a b c (a + b * c)) 1 2 3)
  
  # some pattern matching works, some doesn't
  ((-> 5 10) 5)
  ((-> 5 10) 1)
  ((-> @a a) 1 2 3)
  ((-> @a b {a b}) 1 2 3)
  
  # operator precedence works
  (and (or 1 2) (or 3 4))
  
  # almost anything can be used as an expression
  -> (var foo 5)

  # Arc-style "in"
  (in 1 2 3 4 5)
And some more stuff too, like namespaces and the capability to eval things at compile time. I'll keep adding in things and fixing things as I come across them.
2 points by Pauan 5094 days ago | link | parent | on: Revisiting + vs join

Yeah, definitely. The reason for the bug is because the Arc compiler uses Racket's "append" to do the join, which means it first needs to convert the Arc list to a Racket list, do the append, then convert back to an Arc list.

So there's two ways to fix this:

1) Change Arc to be like Arc/Nu, so that it doesn't need to do the conversion. I don't expect this to be too hard.

2) Write + in Arc, and have it call "join" when the first argument is a list. Then there's no discrepency between the two.

I actually dislike how the overloaded + is written in Arc, so I think having it call join instead would be great.


From what I understand, the "canonical way" is basically "whatever Emacs does", but I don't use Emacs. How I indent code (and how I have seen most other people indent code) is as follows:

2 spaces for indentation. Use indentation after any "block" expression:

  (def foo (x)
    (bar x))

  (let foo 5
    (bar foo))
Non-block expressions should be like so:

  (foo 1 2 3 4)
  
  (foo 1
       2
       3
       4)
  
  (foo 1
    2
    3
    4)
That last one is generally rare and considered somewhat bad form, from what I understand.

"if" indentation is flexible:

  (if 1 2 3)

  (if 1
      2
      3)

  (if 1
        2
      3
        4)

  (if 1  2
      3  4)

  (if 1
    2
    3)
Aside from "if", the Lisp code I've seen is generally very uniform, much more so than the JavaScript code I've seen, which can vary substantially from one person to another. For instance, when reading Racket code, it is usually very readable, and the only difference I've noticed is that in some situations they use 1 space rather than 2.

I think the reason Lisp doesn't have a "standard indentation style" is because it doesn't need one. There's no curly braces or distinction between statements and expressions, so people naturally tend to converge on a single style.

And even in the case of the different styles for "if", I at least choose whichever style works the best for me on a case-by-case basis, so that it looks the best. My general heuristic is as follows:

Use this style when there's only 3 arguments:

  (if 1
      2
      3)
Use this style when there's more than 3 arguments:

  (if 1
        2
      3
        4)
Rarely use this style, only when all the arguments are short:

  (if 1  2
      3  4)
For Nulan, I haven't yet worked out the "if" idioms, but thus far I've used this form exclusively:

  (if 1
    2
    3)

"..a single, canonical way.. like K&R style for C."

I don't follow. Isn't K&R just one possible indentation style for C-like languages? Curly on the same line vs next line, etc.?


It's pretty rare that I don't have syntax highlighting. Another thing about the parens that is annoying is that there doesn't seem to be a single, canonical way for them to interact with indentation, like K&R style for C.

I haven't used ruby, but I think terseness and readability tend to go hand in hand, and ruby looks terse.

Having a sugar -> S-expressions transform is great. It's the best of both worlds.

1 point by akkartik 5094 days ago | link | parent | on: Revisiting + vs join

Yeah I don't disagree. I just found it more fundamental to ask why it should be so subtle to debug.

Also, if the language used immutable conses then every cons would indeed have to change, so there's some rationale for keeping it purely functional.

I do totally agree that the two should be consistent. That sucks a lot.

2 points by zck 5094 days ago | link | parent | on: Revisiting + vs join

It seems ugly that calling '+ changes the elements inside its arguments. Even if 'testify were changed, I still think '+ shouldn't do that.

After I wrote that comment it seemed to me that variadic is more fundamental, because you can always convert lists using apply. But you're right that it's not hard to bounce between the two.

Thanks. Your language looks like one I'd like to try. Getting rid of the outermost set of parens is a good idea; the fewer parens that you have to balance, the better. I haven't tried doing any serious programming in a whitespace-sensitive language. I've always found C very readable -- I think C hit a syntactic sweet spot. A lot of people must agree, judging by how many languages have copied C's style.

PS. The one whitespace-sensitive language I use often is make, if it counts. I like make's syntax.

1 point by akkartik 5095 days ago | link | parent | on: Revisiting + vs join

There'll totally be places where it matters whether something conses or not. But that should reflect in the (longer) name, IMO. cons-minimal-append, mayhap?
2 points by akkartik 5095 days ago | link | parent | on: Revisiting + vs join

OMG, why didn't I fix anarki? Doing so now.

Well, essentially we already have 'avg:list as the variadic version. In fact, it looks like we can shave off two characters:

  (map(fn e avg.e)r t)
  (map avg:list r t)
I don't remember ever using 'avg except maybe to average some time measurements at the REPL, and I think the idiom (avg:accum ...) comes in handy for that. But I generally don't use arithmetic operators in my programs, probably due to the subject matter I choose.

Edit: Here's another opportunity:

  (cdr(mem _ s))
  (cdr:mem _ s)
2 points by rocketnia 5095 days ago | link | parent | on: Revisiting + vs join

"I think it should use iso ."

Of course! A quick Google search for [site:arclanguage.org testify iso] turns up a few places Arc Forumgoers have talked about this before:

http://arclanguage.org/item?id=9183 (aw)

http://arclanguage.org/item?id=13678 (me)

http://arclanguage.org/item?id=13267 (me and you)

These were just my first three search results. :)

---

"A caller of either shouldn't typically care whether they create new conses or not."

Does anyone mutate association lists? It might matter there.


It occurs to me that avg is intended to be analogous to sum. Perhaps we could have two variants, avg and mean. Which one should be variadic, thoughts?

Thanks for the recommendation; I ordered the book.

I'll plug my whitespace-sensitive toy lisp as well: http://github.com/akkartik/wart#readme

  $ git clone http://github.com/akkartik/wart
  $ cd wart
  $ ./wart   # ./wart test runs tons of unit tests
I'd love to have you try it out and tell us if it requires less retraining.

While I agree in general, I've found that S-expressions are easier to read than C syntax when you don't use syntax highlighting because the parens naturally group things together. With syntax highlighting, the situation changes so that syntax-rich languages can be significantly more readable.

I also find Ruby to be extremely readable, with or without syntax highlighting. Of course, I can only imagine how contorted and crazy Ruby's parser must be...

In any case, I tend to agree with Paul Graham on the principle that syntax should just be sugar for S-expressions. For instance, in Nulan:

  -> a b c (a + b * c)
  (&fn (&list a b c) (&add a (&mul b c)))

  foo.bar
  (&get foo "bar")

  [ "foo" 1 "bar" 2 ]
  (&dict "foo" 1 "bar" 2)

  !(foo bar @qux)
  (&not (foo bar (&splice qux)))
All the syntax expands to plain old S-expressions which can be manipulated by macros.
1 point by akkartik 5095 days ago | link | parent | on: Revisiting + vs join

It took me a while to understand why this should be. cartesian calls mem which calls testify, and testify uses is.

  (def testify (x)
    (if (isa x 'fn) x [is _ x]))
I think it should use iso. Can y'all think of any reasons that's a bad idea?

If we make that change, I'm not too concerned about whether + is like join. A caller of either shouldn't typically care whether they create new conses or not.

More