单子文件 I/O

Monadic File I/O

有很多关于如何读取和写入文件的示例,但许多帖子似乎已经过时,are too complicated, or are not 'safe' (1, 2)(它们 throw/raise 例外)。来自 Rust,我想用像 result.

这样的 monadic 明确地处理所有错误

以下是 'safe-er' 的尝试,因为打开 read/write 不会 throw/raise。但不确定关闭是否会失败。有没有更简洁、可能更安全的方法来做到这一点?

(* opam install core batteries *)
open Stdio
open Batteries
open BatResult.Infix

let read_safe (file_path: string): (string, exn) BatPervasives.result =
  (try let chan = In_channel.create file_path in Ok(chan)
  with (e: exn) -> Error(e))
  >>= fun chan -> 
    let res_strings = 
      try 
        let b = In_channel.input_lines chan in
          Ok(b) 
      with (e: exn) -> Error(e) in
    In_channel.close chan;
    BatResult.map (fun strings -> String.concat "\n" strings) res_strings

let write_safe (file_path: string) (text: string) : (unit, exn) BatPervasives.result =
    (try 
      (let chan = Out_channel.create file_path in Ok(chan))
    with (e: exn) -> Error(e))
    >>= fun chan -> 
      let res = 
        (try let b = Out_channel.output_string chan text in Ok(b) 
        with (e: exn) -> Error(e)) in
      Out_channel.close chan;
      res
  
let () =
  let out = 
    read_safe "test-in.txt"
    >>= fun str -> write_safe "test-out.txt" str in
  BatResult.iter_error (fun e -> print_endline (Base.Exn.to_string e)) out

作为Janestreet工业级标准库的一部分的Stdio库已经提供了这样的功能,当然是安全的,例如In_channel.read_all reads the contents of the file to a string and corresponding Out_channel.write_all将其写入文件,所以我们可以实现一个 cp 实用程序,

(* file cp.ml *)
(* file cp.ml *)
open Base
open Stdio

let () = match Sys.get_argv () with
  | [|_cp; src; dst |] ->
    Out_channel.write_all dst
      ~data:(In_channel.read_all src)
  | _ -> invalid_arg "Usage: cp src dst"

要构建和 运行 代码,请将其放入 cp.ml 文件(最好放在一个全新的目录中),然后 运行

dune init exe cp --libs=base,stdio

此命令将 bootstrap 您的项目使用 dune。然后你可以运行你的程序

dune exec ./cp.exe cp.ml cp.copy.ml

这里是 link 到 OCaml Documentation Hub,这将使您更容易在 OCaml 中找到有趣的库。

此外,如果您想将引发异常的函数转换为 returns 错误的函数,您可以使用 Result.try_with,例如,

let safe_read file = Result.try_with @@ fun () ->
  In_channel.read_all file

这是基于 的完整安全解决方案,仅使用 Base 库。

open Base
open Base.Result
open Stdio

let read_safe (file_path: string) =
  Result.try_with @@ fun () ->
    In_channel.read_all file_path

let write_safe (file_path: string) (text: string) =
  Result.try_with @@ fun () ->
    Out_channel.write_all ~data:text file_path   

let () =
  let out = 
    read_safe "test-in.txt"
    >>= fun str -> 
      write_safe "test-out.txt" str in
  iter_error out ~f:(fun e -> print_endline (Base.Exn.to_string e))

您可以在 OCaml 中读取和写入文件,而无需其他标准库。您需要的一切都已内置于 OCaml 附带的 Stdlib 中。

下面是一个读取文件的示例,同时确保文件描述符在发生异常时安全关闭:。从那里您可以编写一个类似的函数来使用相应的函数 open_outout_channel_lengthoutput.

来写入文件

这些以 OCaml 的 bytes 类型读写文件内容,即可变字节串。但是,它们可能会抛出异常。这可以。在 OCaml 中,异常是廉价且易于处理的。然而,有时人们出于某种原因不喜欢它们。因此,现在给抛出异常的函数添加后缀 _exn 有点惯例。所以假设你这样定义上面提到的两个函数:

val get_contents_exn : string -> bytes
val set_contents_exn : string -> bytes -> unit

现在您(或任何人)可以轻松地包装它们并 return 一个 result 值,就像 Rust 一样。但是,由于我们在 OCaml 中有多态变体,我们利用它来组合可以 return result 值的函数,如下所述:https://keleshev.com/composable-error-handling-in-ocaml

所以你可以这样包装它们:

let get_contents filename =
  try Ok (get_contents_exn filename) with exn -> Error (`Exn exn)

let set_contents filename contents =
  try Ok (set_contents_exn filename contents) with exn -> Error (`Exn exn)

现在这些有以下类型:

val get_contents : string -> (bytes, [> `Exn of exn]) result
val set_contents : string -> bytes -> (unit, [> `Exn of exn]) result

并且它们可以与其他函数组合在一起,return result 值具有多态变体错误通道。

我想在这里提出的一点是为您的用户提供这两种方式,这样他们就可以选择对他们有意义的方式——例外或 results。