I finally figured it out. Inside "..." is an flink whose continuation function references x. srv.arc's default threshold for harvest-fnids is very large, so my memory was getting completely consumed before any fnids could be harvested; hence the references to each x were lingering, and mzscheme couldn't consider them garbage.
My workaround is to have foo make a call to harvest-fnids with a calculated threshold much lower than the default, and now memory consumption isn't getting out of hand. Thanks for everyone's help!
The "append" requirement is pretty specific. The file has to be opened for appending, if that's possible. The Python 2 and Groovy examples have complaints along these lines in the comments.
Furthermore, even though I think it was vague in the original description of the problem, the complaints indicate that the "world" line should be read into a variable.
So the first thing I tried was this, riffing off of evanrmurphy's version:
No luck. The file contains "helloworld" on one line. It doesn't even have a trailing newline. This is thanks to a lack of flushing, which is fixed on Anarki.
Speaking of bugs, do we even close these file handles?
And what if someone's mysteriously edited the file in between our commands, and we end up reading in a million-line file or barfing on a paren mismatch?
Also, most of the responses seem to use "putStrLn", "println", etc. for displaying the line that was read, even though that sacrifices two characters' worth of brevity. :-p
Let's try that again.
(w/outfile f "fileio.txt" (disp "hello\n" f))
(w/appendfile f "fileio.txt" (disp "world\n" f))
(w/infile f "fileio.txt" (repeat 2 (= line readline.f)))
prn.line
Whoops, I have a stray #\return at the end, 'cause I'm on Windows. To account for that, I can use Anarki and thereby take advantage of aw's 'readline fix and the flushing fix I mentioned earlier. If I use Anarki and take garply's lib/util.arc suggestion, this is what I get:
That's what I'd settle for in this I/O demonstration, but if it were my own code I'd end up using Lathe, just so I could continue to support official Arc on Windows:
(= lathe-dir* "my/path/to/lathe/arc/")
(load:+ lathe-dir* "loadfirst.arc")
(use-fromwds-as ut (+ lathe-dir* "utils.arc"))
(w/outfile f "fileio.txt" (disp "hello\n" f))
(w/appendfile f "fileio.txt" (disp "world\n" f))
(w/infile f "fileio.txt" (repeat 2 (= line ut.readwine.f)))
prn.line
Unfortunately, this code will break on Jarc 17 and Rainbow, and it's really their fault. :-p Mainly, both of them seem to overwrite the file rather than appending to it, and no workaround for that is coming to mind--well, except for ignoring that part of the problem statement. I'll start a new bug report thread.
For what it's worth, here's how I'd write it in Groovy:
def file = "fileio.txt" as File
file.withWriter { it.writeLine "hello" }
file.withWriterAppend { it.writeLine "world" }
def line = file.withReader { it.readLine(); it.readLine() }
println line
My favorite posted answer is this PowerShell one. The comments lead me to believe it's cheating somehow, but the brevity is really impressive. This is pretty much what I'd expect a file I/O DSL to look like.
1. It specifically says "Append the second line "world" to the file." Not just print those two lines, but print the first line and then append the second line to the file.
2. Your code doesn't close the output port. This really isn't a problem in a basic example like this, but if you did it repeatedly...
arc> (repeat 4000 (w/stdout (outfile "fileio.txt") prn!hello prn!world))
Error: "open-output-file: cannot open output file: \"/[elided]/fileio.txt\" (Too many open files; errno=24)"
Most of the examples on that page do append and do close their files. So... here's my version, which I think is more correct.
(w/outfile f "fileio.txt"
(disp "hello\n" f))
(w/appendfile f "fileio.txt"
(disp "world\n" f))
(pr:cadr:readfile "fileio.txt")
Incidentally, "(w/stdout f prn!hello)" is just a little bit longer than "(disp "hello\n" f)".
1. It specifically says "Append the second line "world" to the file." Not just print those two lines, but print the first line and then append the second line to the file.
True, but then under Clarification, it says: "You don't need to reopen the text file after writing the first line." I'd thought that continuous write would pass for writing and then appending with the same file open, but I guess it doesn't.
The non-closed output port is a very good point though, and that disp makes it shorter is clever.
Cool, I have been attributed! :-) I feel I have to make another embarrassing nitpick, though, which is that we want the second line, rather than the second s-expression (they happen to be the same here, but the task is to demonstrate how to do these things). I would therefore go for one of these as the third piece of code:
(w/infile f "fileio.txt"
(pr:cadr:lines:allchars f))
; Or, though this doesn't close the input port:
(pr:cadr:lines:allchars:infile "fileio.txt")
; Both of the above, though fun, will read all characters
; in the file, only to return the second line. Better:
(w/infile f "fileio.txt"
readline.f
(prn readline.f))
4. Read the second line "world" into an input string.
5. Print the input string to the console.
Is an "input string" a variable? I guess it can't be much else... Ok. And I would put "prn.l" instead of "(prn l)"--not that it makes much difference, but I really like using ssyntax. Otherwise, I think I'm satisfied with this code.
And we've all written variations on this here, don't worry about calling it mine. It's a piece of code that we are collectively beating (or artfully crafting) into shape.
Never fear! I'll show up to flog this horse in the nick of time! Y'know, before rigor mortis sets in.
Here's another I/O utility I think would be useful.
(def readlines (n (o str (stdin)))
(let line nil
(repeat n (= line (readline str)))
line))
It undoubtedly sets a variable (just not a global), though I think the "challenger" phrased the requirement as such because of a C-centric view: allocate a chunk of memory for the string, then read the string into there (which, technically, even a simple (readline) does). Anyway... With all of these, the Arc code would look something like
And I don't think I could squeeze more out of that without getting overly specific. Of course, readlines is a conventional name for something that just reads all the lines of a stream, but I think we could reasonably use names closer to Arc's allchars and filechars.
Hm. Anarki calls them w/stdoutfile and w/stdinfile, but wouldn't tofile and fromfile be more consistent with tostring and fromstring (and shorter, besides)? Then there's the whole appending business. Can't really give an optional parameter to tofile like outfile has, and toappendfile runs together horribly. What about just appendfile?
Then, using readline instead of readfile so that it (1) doesn't try to parse things as s-exprs and (2) doesn't read the entire file for no good reason, I'd envision it thus:
I'm not sure about the appendfile--given what infile and outfile do, it sounds like a procedure that creates an output-port that appends to a file. Maybe to-appendfile, appendtofile, appendingfile, tofile/append... Alternatively, we could make keyword arguments happen in Arc, and then you would just throw ":append t" or something inside the call to tofile. That would also allow for further extension with, e.g., an :if-exists argument.
How about 'tolog? Are files opened for appending for other reasons, in practice? This would also keep with the to-means-output, from-means-input pattern.
I'd try to err on the side of generality. And I'm not quite as concerned about to:output / from:input, if the names are still "clear enough".
As to waterhouse's suggestions, I had considered those names. I suppose if you read appendfile as a noun instead of a verb-and-noun, it's confusing (though infile and outfile don't really have the same problem, so it's not the train of thought my brain follows). It's hard modifying a name like tofile with a long word like append. We already have two words in tofile, so adding a third without hyphenation is stretching it, and adding hyphens breaks the flow with the other names (fromfile, tostring, etc.). We could go for something shorter, like addtofile, which delineates itself well without hyphens because each word is one syllable. If we can't avoid hyphens, using / instead (e.g., tofile/a or tofile/append) flows better, but isn't that great.
Another name that occurred to me -- and is probably my favorite so far -- is ontofile, which is still simple enough to not need hyphens, communicates intent (appending something onto a file), and worms the word to in there, painting it with the to:output / from:input correlation. Thoughts?
Another name that occurred to me -- and is probably my favorite so far -- is ontofile, which is still simple enough to not need hyphens, communicates intent (appending something onto a file), and worms the word to in there, painting it with the to:output / from:input correlation. Thoughts?
+1! ontofile is a great name, in my opinion, for all the reasons you listed.
I searched for a good portmanteau in the vein of mappend, but I don't think there is one. fappend? Sounds like frappuchino. filepend is decent, but I think I prefer ontofile.
I put ($.collect-garbage) at the top of my defop and tested again. Memory leaks just the same. Somebody in Arcland is forgetting to take the trash out because Scheme has none to collect. :P
Nice macro, by the way. I added it to my utils with an attribution.
Maybe you should try out a more recent version of Racket?
That's a good idea. I'm still using MzScheme 4.2.1. (Although before I go to the trouble, maybe I should skim recent release notes to see if they were supposed to have fixed any memory leak.)
Thanks for doing that test. I just tried again on my case and it got to > 450 MB (90%+ on a 512 machine) without showing signs of GCing! I'll push it even further later to see if it collects; if it does, then maybe I just need to lower a global like threadlimit* if I don't want the entire machine to get consumed.
Yes and yes. With pg's code, I kept having to make ugly hacks to things like respond to be able to do what I wanted. It was hard to modify, hard to maintain, and hard to debug. IMO, palsecam's code is much more flexible and much better written. I highly recommend it.
I can definitely imagine better documentation easing the learning curve for new Arc programmers. Source code can be self-explanatory, but only after you've attained a certain base level of fluency with the language. Just as the axioms have to be defined in Racket [1] before Arc can work, they have to be defined in your head before it can make any sense. So I applaud your effort.
One resource that really helped me learn faster was rntz's help system, available in Anarki [2]. It's very convenient because it gives you access to documentation right at the repl:
arc> help.acons
[fn] (acons x)
Determines if `x' is a `cons' cell or list.
Unlike 'alist, this function will return nil if given an empty list
See also [[atom]] [[alist]] [[dotted]] [[isa]] [[cons]] [[list]]
nil
arc> help.alist
[fn] (alist x)
Return true if argument is a possibly empty list
Unlike 'acons, this function returns t when given an empty list
See also [[atom]] [[acons]] [[dotted]] [[isa]] [[cons]] [[list]]
nil
arc> help.afn
[mac] (afn parms . body)
Creates a function which calls itself with the name `self'.
See also [[fn]] [[rfn]] [[aif]] [[awhen]] [[aand]]
nil
Source code can be self-explanatory, but only after you've attained a certain base level of fluency with the language. Just as the axioms have to be defined in Racket before Arc can work, they have to be defined in your head before it can make any sense. So I applaud your effort.
Well put. My thoughts exactly. The suggestion wasn't necessarily to learn Arc by spending a couple days reading the source. I mean, that's what I did, but I was already familiar with Common Lisp. Just that once you reach a certain point (as Preston seems to have), it's not so daunting to say "hey, I wonder what filechars does?" and go read
(def filechars (name)
(w/infile s name (allchars s)))