3

There are many examples of how to read from and write to files, but many posts seem out of date, are too complicated, or are not 'safe' (1, 2) (they throw/raise exceptions). Coming from Rust, I'd like to explicitly handle all errors with something monadic like result.

Below is an attempt that is 'safe-er' because an open and read/write will not throw/raise. But not sure whether the close can fail. Is there a more concise and potentially safer way to do this?

(* 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

glennsl
  • 28,186
  • 12
  • 57
  • 75
Greg
  • 3,086
  • 3
  • 26
  • 39
  • 1
    There are several issues with this code. However, Stack Overflow is not a code review forum. I strongly recommend closing the question here and asking on the OCaml Forum. Many people there eager to help out. – Yawar May 26 '21 at 22:33
  • 1
    The glaring issue is that you're mixing different standard libraries. I would suggest you to stick to one. You can drop by discuss.ocaml.org for a more thorough discussion. – ivg May 26 '21 at 23:32
  • 1
    In addition to the other comments and answers which have been given, since you are concerned with exception safety note that if you go down the route of using exceptions, ocaml has a Fun.protect function which behaves much like unwind-protect (and finally clauses for that matter). – Chris Vine May 26 '21 at 23:48
  • 2
    @yawar Sorry and I know. The question really isn't intended to get a code review. All I'm trying to do is generate some more helpful, modern content, so others can benefit. Every google search on basic functionality is garbage compared to other langs (js, c#, rust, etc). Just helping the next beginner (or me in two months). – Greg May 27 '21 at 03:49
  • Indeed, we're missing those simple snippets, questions and answers for typical programming tasks, that are present for other languages. Therefore I support your initiative. – ivg May 27 '21 at 12:57
  • Just for completeness: note that there is also: https://codereview.stackexchange.com/ – GhostCat May 27 '21 at 17:09

3 Answers3

4

The Stdio library, which is a part of the Janestreet industrial-strength standard library, already provides such functions, which are, of course safe, e.g., In_channel.read_all reads the contents of the file to a string and corresponding Out_channel.write_all writes it to a file, so we can implement a cp utility as,

(* 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"

To build and run the code, put it in the cp.ml file (ideally in a fresh new directory), and run

dune init exe cp --libs=base,stdio

this command will bootstrap your project using dune. Then you can run your program with

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

Here is the link to the OCaml Documentation Hub that will make it easier for you to find interesting libraries in OCaml.

Also, if you want to turn a function that raises an exception to a function that returns an error instead, you can use Result.try_with, e.g.,

let safe_read file = Result.try_with @@ fun () ->
  In_channel.read_all file
ivg
  • 34,431
  • 2
  • 35
  • 63
2

You can read and write files in OCaml without needing alternative standard libraries. Everything you need is already built into Stdlib which ships with OCaml.

Here's an example of reading a file while ensuring the file descriptor gets closed safely in case of an exception: https://stackoverflow.com/a/67607879/20371 . From there you can write a similar function to write a file using the corresponding functions open_out, out_channel_length, and output.

These read and write file contents as OCaml's bytes type, i.e. mutable bytestrings. However, they may throw exceptions. This is fine. In OCaml exceptions are cheap and easy to handle. Nevertheless, sometimes people don't like them for whatever reason. So it's a bit of a convention nowadays to suffix functions which throw exceptions with _exn. So suppose you define the above-mentioned two functions as such:

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

Now it's easy for you (or anyone) to wrap them and return a result value, like Rust. But, since we have polymorphic variants in OCaml, we take advantage of that to compose together functions which can return result values, as described here: https://keleshev.com/composable-error-handling-in-ocaml

So you can wrap them like this:

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)

Now these have the types:

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

And they can be composed together with each other and other functions which return result values with a polymorphic variant error channel.

One point I am trying to make here is to offer your users both, so they can choose whichever way–exceptions or results–makes sense for them.

Yawar
  • 11,272
  • 4
  • 48
  • 80
1

Here's the full safe solution based on @ivg answer, using only the Base library.

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))
Nimantha
  • 6,405
  • 6
  • 28
  • 69
Greg
  • 3,086
  • 3
  • 26
  • 39