2) This helps out a ton with arc2js. In arc2js, now all I need to do is provide an `=` macro and I'll get zap, or=, push, pull, swap, etc. all for free. Without this change, I'd have to define my own custom versions of zap, or=, etc...
"It truly does not make any sense to me why zap is defined like this"
'zap is the only use I typically have for the 'setforms "binds" list (which ensures the subexpressions of 'place are only evaluated once).
Still, 'zap doesn't need to work that way: as long as I'm using a language where I know 'zap evaluates its place twice, I'm pretty much okay with it. It's a wart, but it's not an impediment.
To explore Arc-3.1-like options for a bit, here's a cleanup of Arc 3.1's definition of 'zap:
(mac zap (op place . args)
(with (gop (uniq)
gargs (map [uniq] args)
(binds val setter) setforms.place)
`(atwiths (,@binds ,gop ,op ,@(mappend list gargs args))
(,setter (,gop ,val ,@gargs)))))
If we allow 'setter and 'val to compile and evaluate before 'op and 'args, it gets shorter:
(mac zap (op place . args)
(let (binds val setter) setforms.place
`(atwiths ,binds
(,setter (,op ,val ,@args)))))
I prefer to arrange the compilation and evaluation orders from left to right ('op, 'place, 'args), using a technique like this:
(mac place (place)
(let (binds val setter) setforms.place
`(withs ,binds
(list (fn () ,val) ,setter))))
(mac zap (op place . args)
`(atomic:fn-zap ,op (place ,place) (list ,@args)))
(def fn-zap (op (getter setter) args)
(setter:apply op (getter) args))
"'zap is the only use I typically have for the 'setforms "binds" list (which ensures the subexpressions of 'place are only evaluated once)."
Hm... yes, you're right, `(zap + (foo (bar qux)) 1)` evaluates `(bar qux)` twice, and I don't see an easy/obvious way to fix that in `=`. I'll need to think about this.
It would appear that the `=` operator is already thread-safe even without `atomic-invoke`. In any case, if you're terribly worried, you can always wrap it yourself.
I just got rid of `setforms` completely[1]: not even `=` uses it anymore. I then reimplemented `=` in a much shorter and clearer way[2].
Arc 3.1 takes 80 lines to implement expand=, but Nu takes only 31. And Nu is much clearer and easier to understand as well. In addition, Nu's output is much shorter and is faster:
All constant-time procedures and operations provided by Racket are
thread-safe because they are atomic. For example, set! assigns to a variable
as an atomic action with respect to all threads, so that no thread can see a
“half-assigned” variable. Similarly, vector-set! assigns to a vector
atomically. The hash-set! procedure is not atomic, but the table is
protected by a lock; see Hash Tables for more information. Port operations
are generally not atomic, but they are thread-safe in the sense that a byte
consumed by one thread from an input port will not be returned also to
another thread, and procedures like port-commit-peeked and write-bytes-avail
offer specific concurrency guarantees.
It mentions that hash table assignment is not thread-safe, however if you then go to the hash table page[1], it says this:
A mutable hash table can be manipulated with hash-ref, hash-set!, and
hash-remove! concurrently by multiple threads, and the operations are
protected by a table-specific semaphore as needed. Three caveats apply,
however [...]
In other words, Racket already handles everything, according to the docs. If you're ever worried enough, or run into any problems, it's not hard to wrap it in `atomic` yourself. I'd rather not have the cost of `atomic-invoke` for every assignment, especially if you run all your code in one thread (like I do).
By the way, Nu doesn't use `set!` for global assignment, so I'm not sure if global assignment in Nu is thread-safe or not. But I'd assume it is, since I think `namespace-set-variable-value!` is constant-time.
This ensures that car.foo, (bar), and (scar gs1 val) all happen without interference in between. I suspect Racket at most protects those on an individual basis.
"In pg-Arc, '= on a variable is 'assign without 'atomic. Where 'atomic comes in is when there's a setforms thing to worry about."
I am aware. It still seems to me that if you're dealing with threads, you should wrap assignment in atomic yourself if you're worried about such things. Code that doesn't deal with threads shouldn't have to use atomic.
Perhaps there should be an `a=` macro that's just like `=` but it calls `atomic`. Hm... I wonder... would it be possible to detect whether code is running in the default thread and if not, automatically wrap it in atomic...? May be more trouble than it's worth, though.
That's what I think. Anyone who cares can say (atomic:= ...) or (atomic:zap ...), so I only see a couple of reasons why we'd want to have the 'atomic implicit:
- We want to use it all the time anyway. (I doubt it, but it's hard to tell. I haven't used threads, and therefore I've never bothered to find a way to squeeze utility out of it.)
- There are people who do care, and they'd be better off if the people who didn't care still used 'atomic by accident. (Again, it's hard for me to tell if this is true.)