Personally, I found I prefer racket's pretty-printing with the horrible hash tables compared to something like {pipe "water" a 1 b 2 c ...} because if you try to evaluate `items` or `profs` you won't have a clue what the data is without pretty-printing.
And it turns out I suck at writing pretty-printers. Someone else do it!
I recommend not expecting `quote` or `quasiquote` to be very useful outside the context of metaprogramming.
Quotation is helpful for metaprogramming in a tangible way, because we can easily refactor code in ways that move parts of it from being quoted to being unquoted or vice versa.
And quotation is limited to metaprogramming in a tangible way, because we can only quote data that's reasonably maintainable in the same format we're maintaining our other code in. For instance, an Arc `quote` or `quasiquote` operation is itself written inside an Arc source code file, which is plain text, so it isn't very useful for quoting graphics or audio data.
We can of course use other functions or macros to construct those kinds of data. That's essentially Arc's relationship with tables. When we've constructed tables, we've just used (obj ...) and (listtab ...) and such.
Adding tables to the language syntax is doable, but it could have some quirks.
; Should this cause an error, or should it result in the same thing as
; '(let i 0 `{,++.i "foo"}) or '(let i 0 `{,++.i "foo"})? Each option
; is a little surprising, since any slight edit to the code, like
; replacing one ++.i with (++ i 1), would give us valid code to
; construct a two-entry table, and this code very well might have
; arisen from a slight edit in the opposite direction.
'(let i 0
`{,++.i "foo" ,++.i "bar"})
; Should this always result in 2, or should it result in 1 if "foo"
; comes last in the table's iteration order?
(let x 0
`{"foo" ,(= x 1) "bar" ,(= x 2)}
x)
Personally, I tend to go the other way: I prefer to have as few kinds of data as possible in a language's quotable syntax.
A macroexpander needs to extract two things from the code at any given time: The name of the next macro to expand, and the region of syntax the macro should operate on. Symbols help encode macro names. Lists and symbols together help encode regions of plain text. I think it's for these reasons that symbols and lists are so essential to Arc's syntax.
Arc has other kinds of data that commonly occur in its quotable syntax, like strings and numbers, but it doesn't need them nearly as much as symbols and lists. Arc could expand symbols like |"Hello, world!"| and |123| as ssyntax, or it could simply let everyone put up with writing things like (string '|Hello, world!|) and (int:string '|123|) each time.
Tables would fit particularly well into a language's quotable syntax if they somehow helped encode regions of syntax. For instance, if a macro body consisted of all the files in a directory, then a table could be an appropriate represention of that file collection.
I would be careful not to structure data/logic to accommodate a special syntax.
i.e while using:
pipe!todo.1!id
is certainly fancy, writing a function is just as effective and most likely more performant since it doesn't require read de-structuring:
(fetch pipe todo first id)
So I'm suggesting you shape your syntax usage around your data, not your data around your syntax. You can always write a macro to obtain your desired level of brevity.
So I don't think this is something anybody supports:
arc> (= grail (otable a "up" b "down"))
arc> grail.0
"up"
Ordered associative containers merely have a well-defined order for iterating over. And the order has nothing to do with the order in which elements were inserted.
So what you're asking for is interesting and plausible, but I don't think "ordered tables" is quite the precise label for it.
The general idea behind the fix is that quoted literals need to be treated as data. Arc now has two new functions for this purpose: quoted and unquoted.
The fact that (quote {a 1}) now becomes a hash table is a little strange. I’m not entirely sure that’s correct behavior. It depends whether (car '({a 1})) should yield a hash table. It seems like it should, which is reified in the code now.
EDIT: Ok, I've force-pushed the fixed commit. (Sorry for the force-push.)
If you `git reset --hard HEAD~1 && git pull` it should work now.
The main issue with alists is that the special syntax doesn't work and the notation is so verbose . . . I don't know if the efficiency issues would even come to bear for me.
EDIT: nesting is not behaving as I expect but that may be a product of my own misunderstanding.
I overlooked the existence of `sym` which does make my request altogether superfluous. Higher-order coercion combinators will take some digestion on my part!
Can you give an example of where the notation is better with tables than with alists? Maybe there's something you can do about it, e.g. writing a macro, using `defcall` or `defset`, or extending `sref`.
I almost never use (coerce param 'sym) when I can say sym.param instead. I've always thought Arc doesn't really do enough with the `coerce` function to justify having it in the language; individual functions like `sym` and `string` already do its job more concisely.
---
In practice when I used to write weakly typed utilities in Arc, I tended to find `zap` very nice:
If you're unfamiliar with `zap`, (zap sym param) is essentially (= param (sym param)).
I prefer strong typing these days, but I've sometimes thought this `zap` technique could be refined to put the coercions in the argument list directly. Arc has (o arg default) for optional arguments, and we could imagine a similar (z coercion arg) for automatically zapping an argument as it comes in:
Something else that's been on my mind is that it could be useful to have higher-order coercion combinators.
Racket code can use `->` to build a contract for a function out of contracts for its arguments and its result. The result of (-> string? string? symbol?) is a contract that verifies a value is a two-argument function and then replaces it with a function that delegates to that one, but which verifies that the arguments are strings and that the result is a symbol.
The same thing could be done for coercions: The result of (as-> string string sym) could be a coercion function that coerces its argument into a two-argument function that delegates to the original value, but which first coerces the arguments to strings and then coerces the result to a symbol.
Similarly, in Racket, `(listof symbol?)` is a contract that checks that a value is a list of symbols, and for coercions we could imagine a corresponding `(aslistof sym)` operation for use in your `(map [coerce _ 'sym] ...)` example.
Sometimes Arc's weak typing suffers from making poor guesses as to whether `nil` is intended as a symbol or as a list (not to mention as a boolean), and it takes some adjusting to work around it:
Yeah, in clojure we have the standard hash-maps, but we also have array-maps (which maintain the insertion order) and sorted-maps (which are sorted by keys).
In the last ten years I've only needed ordered maps once or twice. In one case it was for a custom query language I wrote that generated queries from data alone.
eg. (query db :users (array-map :gender "female" :name "xena"))
so in this example adding the :gender clause first would restrict the query and improve performance.
I also remember, in my early clojure days, I made the mistake of relying on standard hash-maps:
eg. (query db :users {:gender "female" :name "xena"})
Until I discovered a query where the performance died and found out hash-maps are actually array-maps (under the hood) until 9 entries. It then auto converts to real hash-maps and loses its order (Clojure does this because maintaining order on bigger data sets is costly from an efficiency/memory perspective).
That's starting to get outside the look and feel of a Lisp. Usually you look to the start of any form to see what it is. The fact that quoted lists can get a post-processor feels more like Forth.
And the `/sym` somehow now has to automatically do different things for a list or other expression? That feels more like APL than Lisp.
I'm skeptical. Are you doing coercions that often? That seems like bad practice. But if you share a little program or app, that may persuade me otherwise.
For coercions, I usually use the `as` alias:
(as type expr)
rather than
(coerce expr 'type)
I find it a lot more convenient because `type` is usually atomic while `expr` can be arbitrarily complex. It makes for a more readable result for the short arguments to come first.
In Anarki, `as` is more common than `coerce`, though we still do a lot of `coerce` that could probably stand to be cleaned up:
> I now have it on my todo list to determine how much the standard library of Lumen matches the names Arc uses.
Lumen looks awesome. I was looking for the docs to determine how much of arc actually exists within lumen, but I couldn't find anything. So if you do this, please let me know.
Also, If I can get some time down the road, I'd like to implement some basic dom manipulation functions. Personally I see Lumen as the best means to do mobile app development in Arc (which is probably one of the best things that can happen for Arc IMHO). Arc on the server side, Lumen on the client side and a code base that's useable by both would be really nice.
I can't speak to elisp, but the way macro systems work in Arc and Racket, the code inside a macro call could mean something completely different depending on the macro. Some macros could quote it, compile it in their own way, etc. So any code occurring in a macro call generally can't be transformed without changing the meaning of the program. Trying to detect and process other macro calls inside there is unreliable.
I have ideas in mind for how macro systems can express "Okay, this macro call is over; everything beyond this point in the s-expression is an expression." But that doesn't help with Arc or Racket, whose macro systems aren't designed for that.
So something like your situation, where you need to walk the code before knowing which macroexpander to subject each part of it to, can't reliably treat the code as code. It's better to treat the code as a meaningless soup of symbols and parentheses (or even as a string). You can walk through the data and find things like `(%language ...)` and treat those as escape sequences.
(What elisp is doing there looks like custom escape sequences, which I think is ultimately a more concise way of doing things if new macro definitions are rare. It gets into a middle ground between having s-expression soup and having a macro system that's designed for letting code be walked like this.)
Processing the scope of variables is a little difficult, so my escape sequences would be a bit more verbose than your example. It's not like we can't take a Racket expression and infer its free variables, but we can only do that if we're ready to call the Racket macroexpander, which isn't part of the approach I'm describing.
(I heard elisp is lexically scoped these days. Is that right?)
This is how I'd modify the escape sequences. This way it's clear what variables are passing between languages:
(%language arc ()
(let in (instring " foo")
(%language scm ((in in))
(let-values (((a b c) (port-next-location in)))
(%language el ((c c))
(with-current-buffer (generate-new-buffer "bar")
(insert (prin1-to-string c))
(current-buffer)))))))
Actually, instead of just (in in), I might also specify a named strategy for how to convert that value from an Arc value to a Racket value.
Anyhow, once we walk over this and process the expressions, we can wind up with generated code like this:
We also collect enough metadata in the process that we can write harnesses to call these blocks at the right times with the right values.
This is a general-purpose technique that should help with any combination of languages. It doesn't matter if they run in the same address space or anything; that kind of detail only changes what options you have for value marshalling strategies.
I think there's a somewhat more convenient approach that might be possible between Arc and Racket, since their macroexpanders both run in the same process and can trade off with each other: We can have an Arc macro that expands its body as Racket code (essentially Anarki's `$`) and a Racket macro that expands its body as Arc code. But there are some difficulties designing the latter, particularly in terms of Racket's approach to hygiene and its local macros, two things the Arc macroexpander has zero concept of. When we switch from Racket to Arc and back to Racket, the hygiene information and local macro scopes will probably be obliterated.
In your arcmacs project, I guess you might also be able to have an Arc macro that expands its body as elisp code, an elisp macro that expands its body as Racket code, etc. :-p So maybe that's the approach you really want to take with `%language` and I'm off on a tangent with this "escape sequence" interpretation.
You're hitting on a problem I've been thinking about for years. There are a few reasons this is tricky, notably related to detecting whether something is a variable reference or a variable declaration.
(%language arc
(let in (instring " foo")
(%language scm
(let-values (((a b c) (port-next-location in)))
(%language el
(with-current-buffer (generate-new-buffer "bar")
(insert (prin1-to-string c))
(current-buffer)))))))
To handle this example, you'll need to know whether each form is a function call, a variable definition, a list of definitions (let-values), a function call, and which target the function is being called for.
For example, an arc function call needs to expand into `(ar-apply foo ...)`
And due to syntax, you can't just handle all the cases by writing some hypothetical very-smart `ar-apply` function. If your arc compiler targets elisp, it's tempting to try something like this:
(ar-apply let (ar-apply (ar-apply a (list 1))) ...
which can collapse nicely back down to
(let ((a 1)) ...)
in other words, it's tempting to try to defer the "syntax concern" until after you've walked the code and expanded all the macros. Then you'd collapse the resulting expressions back down to the language-specific syntax.
But it quickly becomes apparent that this is a bad idea.
Another alternative is to have a "standard language" which all the nested languages must transpile tO:
(%let in (%call instring " foo")
(%let (a b c) (%call port-next-location in)
(|with-current-buffer| (%call generate-new-buffer "bar")
(%call insert (prin1-to-string c)
(%call current-buffer)))))
Now, that seems much better! You can take those expressions and easily spit out code for scheme, elisp, arc, or any other target. And from there it's just a matter of adding shims on each runtime.
The tricky case to note in the above example is with-current-buffer. It's an elisp macro, meaning it has to end up in functional position like (with-current-buffer ...) rather than something like (funcall #'with-current-buffer ...)
There are two ways to deal with this case. One is to hook into elisp's macroexpand function and expand the macros as you go. Emacs calls this eager macroexpansion, and there are some cases related to autoloading (I think?) that make this not necessarily a good idea.
The other way is to punt, and have the user indicate "this is an elisp form; do not mess with it."
The idea is that if the symbol in functional position is surrounded by pipe chars, then the compiler should leave its position alone but compile the arguments. So
Then you'll be in for a nasty surprise: not only does it look visually awful and annoying to write, but it won't work at all, because it'll compile to something like this:
(let (ar-funcall2 (a 1) (b 2))
(ar-funcall2 _+ a b))
I am not sure it's possible to escape the "syntax concern". Emacs itself had to deal with it for user macros. And the solution is unfortunately to specify the syntax of every form explicitly: