This is very similar to a macro in Arubic-style namespaces:
(eval-w/ foo ...)
The above evaluates the expression "..." in the "foo" namespace, which may potentially be a table. That's not quite right, though. Here's the correct version:
(eval-w/ (new-namespace foo namespace)
...)
The above could then be easily wrapped in a macro like "w/table", or even overloading the "with" macro as you did.
---
"..it cranks for a good second and a half, and conses over a million cells."
I'm not entirely sure how such a simple expression could cause so much consing. What is wart doing with all those cells?
Because that would require hash table's keys being sorted, and then the results would vary depending on what keys the hash table has. For instance:
(let (a b c) (obj d 4 c 3 b 2 a 1)
(list a b c))
Should that be (3 2 1) or (1 2 3)? Or maybe (4 3 2)? But, that's assuming hash table keys are sorted in the first place, when they aren't.
In addition, that would require a runtime check to determine whether the argument is a hash table or a list. With my method, the check is done at compile-time: no runtime costs.
Also, it works on more than just hash tables. In fact, it works on anything that accepts 1-2 arguments, where the first is a symbol. Thus, if you created a new data type, you could use keyword destructuring on it. Basically, this:
(let (:a :b :c) (obj a 1 b 2 c 3)
(list a b c))
Compiles into the equivalent of this:
(withs (g1 (obj a 1 b 2 c 3)
a (g1 'a)
b (g1 'b)
c (g1 'c))
(list a b c))
Compare that to the following:
(let (a b c) (list 1 2 3)
(list a b c))
Which compiles into the equivalent of this:
(withs (g1 (list 1 2 3)
a (car g1)
g1 (cdr g1)
b (car g1)
g1 (cdr g1)
c (car g1))
(list a b c))
Interestingly, that means you could create a data type that worked with both kinds of destructuring. For instance, an alist might support the `foo!bar` syntax, and then you would need a way to distinguish between "look this up based on the key" and "look this up based on the index".
As a final note, from a conceptual standpoint, I view keyword arguments as being implemented with an implicit hash table. Thus using keyword syntax to destructure a hash table makes lots of sense to me.
In other words, if these two are equivalent:
(let (a b c) foo ...)
(apply (fn (a b c) ...) foo)
"I view keyword arguments as being implemented with an implicit hash table. Thus using keyword syntax to destructure a hash table makes lots of sense to me."
Interesting. If you make keyword args pervasive and optional like I do, you need a sense of ordering as well in the table, which weakens that imagery.
"Should that be (3 2 1) or (1 2 3)?"
It should be whatever you define it to be :) Making the symbols keywords didn't actually help me avoid that question at first glance.
"With my method, the check is done at compile-time."
But how does it perform the check for this expression?
(let (:a :b :c) h
..)
Are you using the presence of keywords to disambiguate whether or not to insert the g1 (cdr g1) pairs in the withs?
"Interesting. If you make keyword args pervasive and optional like I do, you need a sense of ordering as well in the table, which weakens that imagery."
Oh I'd love to have pervasive keywords like Python (or wart), but it seems Racket doesn't support that. And I don't plan for Nu to become an interpreter. Keywords are optional in Nu, though (or will be, once I've implemented them).
---
"Making the symbols keywords didn't actually help me avoid that question at first glance."
I just like how there's a clean (and visible) separation between "lookup by key" and "lookup by index". It also avoids a runtime check as well, which is nice.
---
"Are you using the presence of keywords to disambiguate whether or not to insert the g1 (cdr g1) pairs in the withs?"
Yes. The code the compiler outputs depends on whether the argument is a keyword or not.
Pros: it's shorter to use because I don't need to repeat the keys.
Cons: it only works with syms. You can't eval an expr that returns a hash. It replaces the runtime check you're concerned about with that super ugly eval-inside-unquote. You can't import just a few of the keys so it feels a little like 'using namespace std' in C++ -- you may not know what vars you're going to get, and you may end up overriding bindings.
Perhaps I should just make it a new term. That would address the first limitation:
You're not going to be able to use (with hash ...) with a local variable 'hash unless you have at least one of these:
- Fexprs. An fexpr implementation of 'with can use the complete value of 'hash as it determines how to treat the unparsed body.
- Static typing with record types, so that a macro can use the type of 'hash as it determines how to treat the unparsed body.
- Some variant of JavaScript-style scope chain semantics, in the sense that a bare variable reference means (or can mean) a field lookup in general. IMO, this would be the most straightforward to add to an Arc-3.1-like compiler, since it's a matter of compiling foo to (scope 'foo), (with foo ...) to (let ((scope (shadow (scope 'foo) scope))) ...), and other scope-related things in their own analogous ways.
(def foo (a b)
(list a b))
(foo 1 2) -> (1 2)
(foo :b 2 :a 1) -> (1 2)
That doesn't work in Racket. You need to explicitly say that the arguments are keywords:
(def foo (:a :b)
(list a b))
In other words, arguments are either positional or keyword-based, but not both at the same time. This is different from Python, which lets you treat an argument as either one: