As i4cu pointed out, it's because write can't handle tables. There _is_ a save-list, but it's called writefile, and it works not just on lists but most primitive datatypes. However, it doesn't work on tables.
The naming isn't very consistent here. Arguably writefile should be called save-file. We should preserve the write prefix for operations that write to opened Racket file ports (https://docs.racket-lang.org/reference/file-ports.html) and save for operations that write to provided filenames. That convention is followed by save-table and write-table.
And of course we could simplify names still further if we took i4cu's suggestion and had write work with tables.
Even if you merge `writefile` and `save-table` like this, but not `readfile1` and `read-table`, then people still need to know, at development time, what type of data is in the file in order to read it, so they might as well use a type-specific way to write it as well. Unfortunately, merging `readfile1` and `read-table` isn't really possible, since their serialized representations overlap; they can't reconstitute information that was never written to the file to begin with.
From a bigger-picture point of view, this seems like it would become a non-issue once Arc had its own reader. I assume the problem with reading tables using `read` is that Racket's reader constructs immutable hashes. An Arc-specific reader would naturally construct Arc's mutable tables instead.
Doesn't Racket's reader give us similar problems in that it reads immutable strings and cons cells, too? So these problems could all be approached as a single project.
In the short term, it's not a project that would need a whole new reader. It could just be an adaptation of Racket's existing reader... something like this:
(define (correcting-arc-read in)
(let loop ([result (read in)])
; TODO: See if this should construct a Racket mutable cons cell
; instead (`mcons`). Right now this just creates an immutable
; one, which should be fine since Arc uses an unsafe technique
; to mutate those.
[(cons a b) (cons (loop a) (loop b))]
[ (? hash?)
(match-lambda [(cons k v) (cons (loop k) (loop v))])
[ (? string?)
; We construct a new mutable string with the same content as
(substring result 0)]
; We handle tagged values, which are represented as mutable
; Racket vectors.
[(? vector?) (list->vector (map loop (vector->list result)))]
; We handle various atomic values. (TODO: Add more of these
; cases until we've accounted for every writable type Arc
; supports. Alternatively, just make this a catch-all
; `[_ result]`.)
[(? number?) result]
[(? symbol?) result])))
Writing Arc's `queue` type might be tricky, since that representation relies on sharing. It's possible queues (and other tagged values in general) should have a customized read and write behavior.
> I assume the problem with reading tables using `read` is that Racket's reader constructs immutable hashes.
Racket also has mutable hashes created using `make-hash` rather than `hash`. It could just be that the tables are not serialised as something Racket reads as mutable hashes when deserialising it back again?
"It could just be that the tables are not serialised as something Racket reads as mutable hashes when deserialising it back again?"
I'm pretty sure the Racket reader never reads a mutable hash, but that it's possible for a custom reader extension to do it.
Some of Racket's approach to module encapsulation depends on syntax objects being deeply immutable. In particular, a module can export a macro that expands to (set! my-private-module-binding 20) but which uses `syntax-protect` so that the client of that macro can't use the `my-private-module-binding` identifier for any other purpose. If the lists constituting a program's syntax were usually mutable, then it would be hard to stop the client from just mutating that expansion to make something like (list my-private-module-binding 20), giving it access to bindings that were meant to be private.
I think this is why Racket's `read-syntax` creates immutable data. As for why `read` does it too, I think it's just a case of `read` being a relatively unused feature in Racket. They don't have many use cases for `read` that they wouldn't rather use `read-syntax` for, so they don't usually have reasons for the behavior of `read` to diverge from the behavior of `read-syntax`.
All this being said, they could pretty easily add built-in syntaxes for mutable hashes, but I think it just hasn't come up. Who ever really wants to read a mutable value? In Racket, where immutable values are well-supported by the core libraries, you aren't gonna need it. In the rare case you want it, it's easy enough to do a deep traversal to build the mutable structure you're looking for (like my example `correcting-arc-read` does).
It only comes up as a particular problem in Arc. Arc's language design doesn't account for the existence of immutable values at all, so working around them when they appear can be a bit quirky.
The Racket reader reads a seven-element list there, not a mutable hash.
Looks like you're proposing to use a two-stage serialization format. One stage is `read` and `write`, and the other is `serialize` and `deserialize`. At the level of language design, what's the point of designing it this way? (Why does Racket design it this way, anyway?)
I can see not wanting to have cyclic syntax or syntax-with-sharing in the core language just because it's a pain in the neck to parse and another pain in the neck to interpret. Maybe that's reason enough to have a separate `racket/serialize` library.
But isn't the main issue here that Arc's `read` creates immutable tables when the rest of the language only deals with mutable ones? The mutable tables go through `write` just fine, but `read` doesn't read the same kind of value that was written out. If and when this situation is improved, I don't see where `racket/serialize` would come into play.
Hmm, all right. I didn't want to believe that was the point you were trying to make. In that case, I think I must not have conveyed anything very clearly in the `correcting-arc-read` comment.
I've been trying to respond to this, which was your response to that one:
"Racket also has mutable hashes created using `make-hash` rather than `hash`. It could just be that the tables are not serialised as something Racket reads as mutable hashes when deserialising it back again?"
In the `correcting-arc-read` comment, I used `make-hash` in the implementation of `correcting-arc-read`, so I assumed your first sentence was for the edification of others. I found something to respond to in the second, which kinda pattern-matched to a question I had on my mind already ("Can't the Racket reader just construct a mutable table since what was written was a mutable table?").
As for my response to your "No..?"...
One of the purposes of `correcting-arc-read` is that (when it's used as a drop-in replacement for Arc's `read`) it makes `readfile1` return mutable tables. So if anyone had to be convinced that a reader that returned mutable hashes could be implemented at all in Racket, I thought I had shown that already. When it looked like you might be trying to convince me of something I had already shown, I dismissed that idea and thought you were instead trying to clarify what your proposed alternative to `correcting-arc-read` was.
Seems like I've been making bad assumptions and that as a result I've been mostly talking to myself. Sorry about that. :)
Maybe I oughta clarify some more of the content of that `correcting-arc-read` comment, but I'm not sure what parts. And do you figure there are any points you were making that I could still respond to? I'd better not try to assume what those are again. :-p
I have an old fork (https://github.com/akkartik/arc) that has an extensible generic pair of functions called serialize and unserialize which emit not just the value but also tagged with its type. read and write are built atop them.