Arc Forumnew | comments | leaders | submitlogin
3 points by rocketnia 5103 days ago | link | parent

I don't know if I speak for thaddeus here, but I do think (obj 'a 1 'b 2) would a better API than (obj a 1 b 2), for a couple of reasons. For one thing, since it's something you may want to use a lot, it helps to be able to continue to use it even in the odd cases where you have keys you want to evaluate.

The other reason's a bit more in-depth: I think it's oftentimes good practice to use gensym keys, and especially gensym types. It helps prevent libraries from conflicting with each other. When using this technique, an 'obj that doesn't evaluate its keys is pretty useless, but an 'obj that does evaluate its keys is as convenient as (obj a 1 b 2) again, where 'a and 'b are variables holding the gensyms.

What I really think is best is to just have both. Looking at my site-generator code, I see that I defined (objal a 1 b 2) in terms of (tabal 'a 1 'b 2). :-p I think that's generally why I make so many macros that are merely cheap abbreviations of functions, actually; I like calling the function versions directly every once in a while, but I like saving a bit of line noise in the common cases.

As for your (= 'foo 42) example, I think that's one place where the quote really doesn't fit. No matter how many times you say (= 'foo 42), you should never witness (is 'foo 42), so you're not really assigning the value of that expression. However, if (= #,foo 42) were short for (= [zap _ foo] 42), which behaved as ([zap _ foo] [do 42]), that would totally make its own kind of sense. Of course, I'd want to have it both ways again, so I'd name that version 'fn-= or something so that '= could stay the same.



2 points by waterhouse 5103 days ago | link

> What I really think is best is to just have both. ... I think that's generally why I make so many macros that are merely cheap abbreviations of functions, actually; I like calling the function versions directly every once in a while, but I like saving a bit of line noise in the common cases.

I agree with this. Compare it with having naming and anaphoric macros: iflet and aif, rfn and afn, even fn and []; I'd add awhile and whilet to the list even though awhile isn't in official Arc, and fromfile, tofile, and ontofile[1].

In fact, it is a very general practice in (functional?) programming to first write a function that handles something basic, then write another function on top of it that's more pleasant to work with. It really is ubiquitous. It's good because it separates the primitives from the high-level interfaces; if you want to change the primitives, you don't need to concern yourself with the high-level interface, and if you want to change the high-level interface, you don't need to concern yourself with the primitives. And macros give you flexibility, the ability to construct pleasant interfaces in ways you can't do with pure functions; for example, functions must evaluate all their arguments once, from left to right, while macros don't have to.

In this case, I don't think we have a function that directly does what you're asking for (i.e. it operates like obj except it evaluates the keys). I consider this a problem. Note that in Scheme and CL, we can go (vector x y z) instead of crap like (let u (make-vector 3) (= u.0 x u.1 y u.2 z) u). We should likewise have a good table-constructing function even if it isn't strictly necessary as a primitive. At the moment, this is the closest we have:

  (apply listtab (pair:list 'a 1 'b 2))
  =
  (obj a 1 b 2)
So I think we should have a function like this:

  (def ???? args
    (apply listtab pair.args)) 
What should we call it? I think it's rare enough to want evaluated keys that we don't need to give it a short name like "obj". Maybe "make-table", "make-hash", "make-obj"... Hmm, there's a function named "table". Given no arguments, (table) returns an empty hash-table. We could make this be our table-constructing function--in fact, I think that makes a lot of sense, as the function defined above should return an empty table when given no arguments. I again draw your attention to the analogy of the vector function in Scheme and CL. Only problem is, the table function currently defined in Arc actually is overloaded: if you give it an argument, it will apply this argument (which should be a function) to the table before returning the table.

  (xdef table (lambda args
                (let ((h (make-hash-table 'equal)))
                  (if (pair? args) ((car args) h))
                  h)))
  --ac.scm
I grepped Arc for '(table ' and found three instances (all in news.arc) where the function table was actually called with an argument [the other matches were, like, (tag (table width "100%" cellspacing ...))]. They look like this:

  (table 
     [each ip ips
       (= (_ ip) (dedup (map !by (+ (bads ip) (goods ip)))))]))

  (table [each-loaded-item i
           (awhen (and i!dead (check (sitename i!url) banned-sites*))
             (push i (_ it)))]))
The raw-Arc way to do this would be

  (let u (table)
     (each ip ips
        (= (u ip) ...))
     u)
and I believe Anarki has a w/table macro that would make it look like this:

  (w/table u
     (each ip ips
        (= (u ip) ...)))
Which, I think, is close enough to the original that PG/RTM (whoever wrote the above code) wouldn't be too displeased if we kicked out the old usage of table. Actually, you know, this alternating-pairs proposed usage only applies when the number of arguments is even; it wouldn't even conflict if we wanted to implement both behaviors: 1 arg -> empty table, even args -> alternating pairs. In fact, it would simplify implementation to do alternating pairs until there are less than 2 args left, and then if there's 1 arg, treat it as a function and apply it to the table, while if there are 0 args, just return the table.

So. Options: (1) Have table do alternating key-value stuff; if there's one argument remaining at the end, apply it to the table; then return the table. (2) Have table do alternating key-value stuff; signal an error if there's an odd number of arguments [and Paul Graham can get used to w/table], or (2a) if there's one argument, apply that function to the table, while if there are an odd number ≥3 of arguments, signal an error. (3) Leave table as is, and find a new name for our function.

I think (1) is the best thing to do at the moment, and (2) is probably a better future goal but I would be fine being overruled on that (its single advantage is error-reporting when you accidentally give table an odd number of arguments). I think (2a) is a stupid hack and (3) is bad. As such, here is an implementation of (1). Replace the old '(xdef table ...' definition in ac.scm with the following:

  (xdef table (lambda args
                (let ((h (make-hash-table 'equal)))
                  (define (slave xs)
                    (cond ((null? xs) 'nonce)
                          ((null? (cdr xs))
                           ((car xs) h))
                          (else (hash-table-put! h (car xs) (cadr xs))
                                (slave (cddr xs)))))
                  (slave args)
                  h)))
  ;Usage
  arc> (table)
  #hash()
  arc> (table [= _!a 1 _!b 2])
  #hash((a . 1) (b . 2))
  arc> (table 'a 1 'b 2)
  #hash((a . 1) (b . 2))
  arc> (table 'a 1 'b 2 [= _!b 3 _!c 4])
  #hash((a . 1) (b . 3) (c . 4))
[1] I mentioned these here: http://arclanguage.org/item?id=12877

-----

2 points by rocketnia 5102 days ago | link

I like all your reasoning, but I think you may have missed this post of mine: http://arclanguage.org/item?id=13236

I gave two ways we can already accomplish the behavior somewhat conveniently even if we don't have something sensible like your version of 'table:

  (copy (table) 'a 1 'b 2)
  (listtab:pair:list 'a 1 'b 2)
(Actually, I used tablist in that post, which was a bug.)

I could have sworn the Anarki version of 'table allowed for (table 'a 1 'b 2) at one point (which inspired me when I made 'objal and 'tabal for my static site generator), but I looked at the Git blame and didn't find any obvious evidence of that....

-----

1 point by waterhouse 5102 days ago | link

Oh, nice. I am not familiar with the copy function, but I missed that "listtab:pair:list" idea. Clever. I'm sure you agree, though, that we should have a nice table function, in the same way that we shouldn't have + defined for us but have to define - ourselves:

  (def - args
     (if (no cdr.args)  ;or no:cdr.args with my recent hack[1]
         (* -1 car.args)
         (+ car.args (* -1 (apply + cdr.args)))))
[1] http://arclanguage.org/item?id=13172

-----

1 point by rocketnia 5102 days ago | link

I honestly wouldn't mind very much if '- were omitted. >.>; Actually, wait. Arc puts every global in the same namespace, so we'd have a few conflicting versions of '- in our respective personal utilities (assuming we don't all just grab the Anarki version), and that would be somewhat horrible.

But yeah, 'table is much better as a function that can actually construct complete tables all at once. ^_^ In this case, it's not a matter of name conflicts between libraries; it's a matter of the most obvious names being taken for the wrong behaviors. Namespaces are still a potential solution, though.

-----

1 point by waterhouse 5102 days ago | link

Namespaces solve the problem flawlessly only if you plan for no one to work on or read anyone else's code, ever.

Taking '- as the example: there are a few non-obvious things about it (how it works with more than 2 arguments, or how it works with 1 argument), and people could reasonably end up with different versions of it (I do think the current version is best, but I wouldn't blame anyone for not thinking about the 3-or-more-variable case or for implementing it differently).

This is not a problem if you use '- as a utility in the definition of your library functions and someone else just imports the exported functions of your library and uses them. But if you intend to, say, paste code on the Arc Forum and expect anyone to understand it, or if you want people to be able to download the source code of your library and make improvements to it, then there's a cost to every non-standard definition you use: the reader has to learn it. If the language doesn't provide "map" and "let" and "with" (which are certainly not primitive; in fact, they're defined in arc.arc), then either you don't use those operators (and your code will suffer for it), or you do use them and that's one more thing the reader needs to learn to understand your code. It's not the end of the world, it's not a game-breaker for all projects, but it's one more obstacle to people understanding your code, and if you can get rid of it, then that is all to the good.

This is why getting people to agree on a good common core language is a good thing even if the language supports namespaces.

-----

1 point by rocketnia 5102 days ago | link

...getting people to agree on a good common core language is a good thing even if the language supports namespaces.

That's a good point. ^_^ I don't use Anarki- or Lathe-specific stuff in my examples if I can help it. And I recently mused on[1] the trend for a language to incorporate a lot of features in the standard so as to improve the common ground, so I should have seen that point to begin with.

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

Namespaces solve the problem flawlessly only if you plan for no one to work on or read anyone else's code, ever.

I've sorta been banking on the idea that everyone programs to their own languages of utilities anyway, and that each of those languages can be talked about, accepted as a common topic, promoted by its proponents, and standardized just like language cores can. Certainly CMSes and game engines have those kinds of communities. :)

Multiple independently developed libraries won't always work together well enough to all be clearly canon components of the language's identity, but I think they are more likely to be interoperable than are things designed from the outset to be entirely new languages.[2] So I don't think it's important for the core libraries to be cutting-edge in practice. The core can fade into the background to a degree.

I haven't thought about this stuff very deeply yet, and it's not rooted in a lot of fact, so please feel free to disillusion me. ^^

[2] I don't mean to suggest there's a well-ordered expected value of interoperability. Whether a library is designed or used in an interoperable or impregnable way is sorta relative and idiosyncratic. One more avenue for interoperability could even be annoying if people come to depend on it in poorly prepared-for ways. ^_^

-----

2 points by waterhouse 5102 days ago | link

Regarding (= 'foo 42): I do think it should be possible to perform assignment on symbols that are chosen at runtime. In Common Lisp, this would be (setf (symbol-function 'foo) 42); in Racket, this is (namespace-set-variable-value! 'foo 42). It's possible to do (eval `(= ,x ',desired-value)) [the ' is necessary because the Arc evaluator doesn't like objects in code], but this feels like a hack and there should be a built-in way to do it (I've said this before, by the way).

What should it be named, and if we wish to use the setf pattern, what should we name the corresponding analogue of "symbol-value"? At the moment, (eval x) does the same thing as (symbol-value x) when x is a symbol [bad things might happen if you do this to 'nil, but otherwise, yeah]. Would (= (eval x) ...) be fine? Or should there be a specialized operator named something like "symbol-value"? For purposes of conservatism, giving a meaning to (= (eval x) ...) seems easiest...

-----

1 point by rocketnia 5102 days ago | link

I immediately thought you were suggesting a way to set local variables at runtime, but this way I have a better answer for you. ^_^

Lathe already defines (= global.x desired-value) so that it behaves just like (eval `(= ,x (',(fn () desired-value)))). As an everyday expression, global.x behaves like (bound&eval x), which makes (zap _ global.x) a bit more intuitive. This utility works on Arc 3.1, Anarki, Rainbow, and Jarc.

I don't mind if something like this is added to the language core too, but isn't that less axiomatic?

-----

1 point by waterhouse 5102 days ago | link

It is certainly less axiomatic--you don't need this to have a working Lisp. However, it's useful (I needed it to do my little module system), it's likely to be very simple to implement as a primitive (given access to Scheme, we can just use 'namespace-set-variable-value!), and its name clearly maps to its function and its setf behavior. (I'd be inclined to use the name "global-value" or "symbol-value", actually.)

Finally, it's easier to see "symbol-value" written in the language description and use it than to figure out how to use the 'eval method properly. Just look at how complex your definition is; even I, who do see why it's necessary to go ',desired-value instead of ,desired-value in my version [if objs in code worked properly, you could use the former version for a while without noticing it was wrong], I don't see why it's necessary to go (',(fn () desired-value)), and I certainly wouldn't think of it myself. (I still don't believe it's actually necessary, but I won't press the point.) Like the case of (++ (car (pop xs))), this is one of the things a core language is good for: the implementor has considered and avoided all the subtle pitfalls for you. (Actually, implementing it as a primitive with the "namespace-..." functions, there aren't any pitfalls to avoid.)

By the way, I don't know if the core of the language is supposed to be all that axiomatic. It should be clean, certainly, but by its nature it should provide more than minimal axioms. In an essay[1], Paul Graham talks about "the core language—those operators that are neither primitives like car and cdr, nor special-purpose library functions. I mean operators like CL's mapcar, let, remove-if-not, and so on."

At any rate, here's what I think: (a) There will be people who need the equivalents of 'symbol-value and (setf symbol-value)--people who write module libraries or debugging facilities or for whatever reason[2] want to determine names at runtime. (b) This might be enough by itself to warrant placement in the core language, but (c) it is extremely easy, not to mention efficient, to implement these as primitives, while it is subtle, error-prone, and inefficient to implement it with 'eval. Therefore, it should be built into the language. (Technically, if it's a primitive, then it's not part of the core language by the implicit definition from the essay, but whatever you call it, it should be built in.)

I feel better about rambling/ranting when I also produce something tangible, like working code. So, here you go:

  (def symbol-value (x)
    ($.namespace-variable-value $.ac-global-name.x))
  
  (defset symbol-value (x)
    (w/uniq g
      (list `(,g ,x)
            `(symbol-value ,x)
            `(fn (val) (($ namespace-set-variable-value!)
                        ($.ac-global-name ,x)
                        val)
               val))))
[1] http://paulgraham.com/core.html

[2] I once did the following in a chess program:

  (each p '(r k n)
    (each a '(a b c d e f g h)
      (each n '(1 2 3 4 5 6 7 8)
        (setf (symbol-function (symb p a n))
              (fn () ...)))))

-----

1 point by rocketnia 5102 days ago | link

I needed it to do my little module system

That's why it's in Lathe, too. ^_^ Since it's there to implement the module system, it's one of only a couple dozen Lathe utilities that isn't in a module, but I find it useful enough that I like it that way.

I don't see why it's necessary to go (',(fn () desired-value))

If you embed a literal vector in an expression (for instance, a tagged value), Racket will evaluate that expression as a copy of the vector. So (let foo (annotate 'a 'b) (= global!x foo) (is global!x foo)) would return nil, which I find frustrating. ^^ (There might be significant mutations-aren't-reflected-in-both-copies gotchas too, but I'm not sure right now.)

If you embed a procedure instead, I don't know if it copies that too, but it doesn't matter 'cause a copy of the procedure should still close over the same bindings. In practice, (let foo (annotate 'a 'b) (= global!x foo) (is global!x foo)) works, so I'm satisfied.

By the way, I don't know if the core of the language is supposed to be all that axiomatic. It should be clean, certainly, but by its nature it should provide more than minimal axioms.

This is a point I just acknowledged but challenged at http://arclanguage.org/item?id=13266. I'm not sure entirely whether and how I (dis)believe it, so a response is welcome. ^_^

it is extremely easy, not to mention efficient, to implement these as primitives, while it is subtle, error-prone, and inefficient to implement it with 'eval.

Sure. Implementing 'symbol-value in terms of 'eval is a pretty big abstraction inversion, and it causes lots of unnecessary consing and compilation at runtime. I'm kinda conviced. ^_^

working code

Incidentally, official Arc allows you to say (assign a.b t) and observe (bound 'a.b). Your 'symbol-value makes it possible to get the value of 'a.b again, whereas there's no non-exploit way to do that with 'eval. ^_^ (You can use the (cdr `(0 . ,_a.b)) bug, lol.)

Speaking of 'bound... why is 'bound a primitive when 'symbol-value isn't? XD Actually, I've wondered why 'bound was a primitive for a while, but that kinda-convinces me further. ^_^

-----

1 point by thaddeus 5102 days ago | link

For the record I prefer:

  (obj 'a 1 'b 2)
keys are not variable names, so I'll ask (in general) why should obj be treated so special or differently?

Again, as you already pointed out, it boils down to flexibility and consistency. I don't see why I would trade saving one character spot over both 1. being clear & consistent and 2. allowing functions to be passed in:

  (obj (look-up-something) 1 (look-up-something-else) 2)

-----

2 points by rocketnia 5102 days ago | link

Right, but once again, adding a few characters may make things more consistent, but does it aid clarity? I really don't know either way.

Using the same heiroglyphics as I posted last time, we can implement 'case entirely as a function:

  ; Hypothetically, this uses the macros 'def, 'whenlet, 'iflet, and
  ; 'do; the functions 'if, 'testify, 'get, and 'apply; the special
  ; forms 'fn, 'zap, and 'quote; and the read macros
  ; #,x for (fn (gs123) (zap gs123 x)), #'x for (fn () x), and
  ; 'x for (quote x).
  
  (def #,case (subject . args)
    (whenlet (first . rest) args
      (iflet (then . rest) rest
        
        ; Check the testified result of (first) against the subject.
        (if (.subject:testify:do.first)
          
          ; Return the result of (then).
          then
          
          #'(apply case-part-2 subject rest))
        
        ; Return the result of (first). It's the last else.
        first)))
  
  ; was (case x y (dosomething))
  (case x #''y dosomething)
This is even an overall brevity boost, I think. What do you think of its clarity? I seriously might try this out in Penknife....

-----

1 point by thaddeus 5102 days ago | link

Not sure. I find the #''y hard on the eyes :)

Though it reminds me a little on how clojure handles inline anonymous functions, which I really like:

    #{+ %1 %2} ;% being the args, %1 being the first, %2 the second, and so on
wondering if something similar is possible with case:

(case x #(myfun)(dosomething))

yet still able to:

(case x 'y (dosomething))

note: Sorry if I missed something and am suggesting non-sense stuff, I haven't yet looked into the code details you provided - so I could be out to lunch.

-----

1 point by rocketnia 5102 days ago | link

In case you missed the post I referred to, here it is: http://arclanguage.org/item?id=13240

All the necessary assumptions are listed in the top comment though (assuming you can intuit my intentions for the 'iflet macro, etc.). It's by no means implemented. ^^

Also, I welcome any switch in the heiroglyphics that makes them more readable. In Penknife, where there are infix operators but no read macros, I'd probably represent #''y as "thunk:sym.y" or "sym.y^". In Arc, ^'y looks like a good choice. Maybe :y or %y could be the zapper (or some better-suited abstraction over setforms).

-----

1 point by evanrmurphy 5101 days ago | link

> Maybe :y or %y could be the zapper (or some better-suited abstraction over setforms).

Maybe something with `=` in it?

-----

1 point by evanrmurphy 5102 days ago | link

> keys are not variable names, so I'll ask (in general) why should obj be treated so special or differently?

There is at least some precedent of special treatment. In JavaScript's object literals, for example, you can do

  {a : 1, b : 2}
instead of

  {'a' : 1, 'b' : 2}
Then again, in other languages like Ruby, the keys must be explicit symbols:

  {:a => 1, :b => 2}
I'm not sure what the prevailing trend is on this (or whether knowing so would help to inform us about which is the better design choice).

-----