fileschemer6rs

Is it possible to load an entire file from disk efficiently in Scheme R6RS?


The following get_file function reads file from disk as a Scheme R6RS string:

; Gets all characters from a port
(define (read_chars_from_port_rev port result)
  (let ((char (read-char port)))
    (if (eof-object? char)
      result
      (read_chars_from_port_rev port (cons char result)))))

; Gets the contents of a file as a string
; If it doesn't exist, returns empty
(define (get_file file)
  (if (file-exists? file)
    (let ((port (open-input-file file)))
      (let ((text (list->string (reverse (read_chars_from_port_rev port '())))))
        (begin
          (close-input-port port)
          text)))
    ""))

It works by opening the file, tail-call-recursively reading char-by-char into a linked list until we find eof, closing the file, then reversing the linked list (because of the tail-call) and converting it to a string.

This procedure should be slow compared to, say, Node.js's readFile, because it reads char by char, and allocates a linked list with one cell for each character in the file. Ideally, we should be able to just read a file as a string buffer, with no dynamic memory allocations.

Is there any way to optimize get_file with the primitives available in R6RS?


Solution

  • You can use get-string-all:

    > (let* ((fp (open-input-file "my-file.txt"))
             (buf (get-string-all fp)))
        (close-port fp)
        (display buf))
    Four score and seven years ago
    our fathers brought forth upon this continent,....
    

    This can be made somewhat more convenient by using call-with-input-file:

    ;;; Returns a string containing the contents of the file `fname`; closes the
    ;;; input port automatically (unless `get-string-all` does not return for
    ;;; some reason).
    (define (get-file fname)
      (call-with-input-file fname get-string-all))
    
    > (get-file "my-file.txt")
    "Four score and seven years ago\nour fathers brought forth upon this continent,....\n"
    

    You can use guard to facilitate returning an empty string when the sought file does not exist (as in the posted code):

    (define (guarded-get-file fname)
      (guard (con
              ((i/o-file-does-not-exist-error? con) ""))
        (call-with-input-file fname get-string-all)))
    
    > (guarded-get-file "my-file.txt")
    "Four score and seven years ago\nour fathers brought forth upon this continent,....\n"
    
    > (guarded-get-file "oops.txt")
    ""