递归读取Racket中的文件

Recursively reading a file in Racket

我正在努力理解如何用球拍逐行读取文件,同时将每一行传递给递归函数。

根据 manual,执行此操作的惯用方法类似于以下示例:

(with-input-from-file "manylines.txt"
    (lambda ()
      (for ([l (in-lines)])
         (op l))))

如果我的函数 op 是一个递归函数,需要根据刚刚从文件中读取的行以及递归历史执行一些复杂的操作怎么办? 例如,我可以有这样一个函数:

(define (op l s)
  ;; l is a string, s is a list
  (cond ((predicate? l)
         (op (next-line-from-file) (cons (function-yes l) s)))
        (else
         (op (next-line-from-file) (append (function-no l) s)))))

我不确定如何在手册描述的框架内使用此功能。 这里 next-line-from-file 是我编造的一个结构,以明确表示我想继续阅读该文件。

我想我可以通过引入副作用来做我想做的事情,例如:

(with-input-from-file "manylines.txt"
  (lambda ()
    (let ((s '()))
      (for ([l (in-lines)])
        (if (predicate? l)
            (let ((prefix (function-yes l)))
              (set-cdr! s s)
              (set-car! s prefix))
            (let ((prefix (function-no l)))
              (set-cdr! prefix s)
              (set-car! s prefix)))))))
 

我实际上没有尝试 运行 这段代码,所以我不确定它是否有效。 无论如何,我敢打赌这个常见任务可以在不引入副作用的情况下解决,但是如何解决?

我最近实现了类似的东西,除了在我的例子中谓词依赖于下一行,而不是前一行。无论如何,我发现最简单的方法是丢弃 in-lines 并递归地使用 read-line。由于谓词依赖于未读输入,因此我使用 peek-string 在输入流中向前看。

如果您真的想使用 in-lines,您可能想尝试使用 sequence-fold:

(sequence-fold your-procedure '() (in-lines))

请注意,这使用了一个累加器,您可以使用它来检查您的过程之前的结果。但是,如果您正在构建一个列表,您通常希望使用 cons 向后构建它,因此最近的元素位于列表的头部并且可以在恒定时间内访问。完成后,反转列表。

Racket 支持的两种方法是将端口变成本质上是行生成器的东西,或者变成流。然后你可以将这些东西作为参数传递给你正在使用的任何函数,以便从文件中连续读取行。

这两个的基础是端口是序列,(in-lines p) returns 另一个序列由 p 中的行组成,然后你可以把它们变成生成器或流。

这是一个使用生成器 cat 文件(换句话说就是读取它的行)的函数:

(define (cat/generator f)
  (call-with-input-file f
    (λ (p)
      (let-values ([(more? next) (sequence-generate (in-lines p))])
        (let loop ([carry-on? (more?)])
          (when carry-on?
            (displayln (next))
            (loop (more?))))))))

此处 call-with-input-file 处理打开文件并使用合适的端口调用其第二个参数。 in-lines 从端口生成一系列线路,然后 sequence-generate 获取任何序列和 returns 两个 thunk:一个告诉您序列是否已用尽,另一个 returns如果不是,接下来的事情。该函数的其余部分仅使用这些函数来打印文件的行。

这是一个使用流执行此操作的等效函数:

(define (cat/stream f)
  (call-with-input-file f
    (λ (p)
      (let loop ([s (sequence->stream (in-lines p))])
        (unless (stream-empty? s)
          (displayln (stream-first s))
          (loop (stream-rest s)))))))

这里的技巧是sequence->stream returns一个stream对应一个sequence,然后stream-empty?会告诉你是否在stream的末尾,如果它不是空的,然后 stream-first returns 第一个元素(概念上是 car)而 stream-rest returns 所有其他元素的流。

我觉得第二个更好。


一件好事是 列表是流 所以你可以编写使用 stream-* 函数的函数,在列表上测试它们,然后在任何其他上使用它们一种流,这意味着任何其他类型的序列,函数永远不会知道。