0

I would like to release a resource when any exception is raised during the usage of the resource.

In C++ this task is easy: I put the release into the destructor, which gets called automatically, whatever happens. In Java one uses the 'finally' clause. What is the practice for this same task in Standard ML?

I tried to catch all exception with a variable pattern 'e' and re-raise it:

datatype FileReadResult = FileReadOkay of string | FileReadError

fun read_file (file_path_string : string) : FileReadResult =
    let
        val istream = TextIO.openIn file_path_string
            (* this file is my resource *)
    in
        TextIO.closeIn istream;
        FileReadOkay "" (* the content of the file will go here *)
        handle e => (TextIO.closeIn istream; raise e)
    end
    handle Io => FileReadError

My compiler (MLton) accepts it, but because I am new in ML, I ask here for some assurance that this is really the right thing | best practice to do.

As this is a common design pattern, I created the below utility function to express it:

(* Uses the given resource in the given way while releasing it if any exception occurs. *)
fun use_resource (resource : 'Resource) (releaser : 'Resource -> unit) (usage : unit -> 'Result) : 'Result = 
    let
        val r = usage ()
    in
        releaser resource;
        r
    end
    handle e => (releaser resource; raise e)

This function plays the same role as the 'using' feature in C#.

libeako
  • 2,324
  • 1
  • 16
  • 20

1 Answers1

3

Yes, that's the usual pattern, with two caveats:

  1. The inner handle is around the FileReadOkay "" only in your code, which won't ever throw. You want to put parentheses around a larger part of the code, so that the handler applies to all of it.
  2. Your outer handler catches Io. I think you mean IO.Io _ here, otherwise you will catch every exception (because Io is just a random fresh variable).

You can also try to abstract it into a function if it occurs frequently. Something along the lines of

(* withTextFile : string -> (TextIO.instream -> 'a) -> 'a
fun withTextFile name f =
    let
        val is = TextIO.openIn name
    in
        (f is before TextIO.closeIn is)
        handle e => (TextIO.closeIn is; raise e)
    end

(The infix operator before evaluates its left-hand and right-hand expression and returns the result of the former). Use it like:

fun echo file = withTextFile file (fn is => print(TextIO.inputAll is))
Andreas Rossberg
  • 34,518
  • 3
  • 61
  • 72
  • Especially thanks for the warning about the Io <-> IO.Io _ error. This "variable pattern" feature of the semantics seems very error-prone - if i make a little typo, then the compiler will not warn and will do something totally different silently. I guess I always should qualify the type in order to avoid it being interpreted as a variable pattern. – libeako Jun 30 '13 at 16:33
  • I agree that this feature is a bit error-prone, at least with exception handlers (with normal patterns, such an error will usually result a "redundant match" warning anyway). I prefer s.th like the OCaml/Haskell solution that distinguishes constructors and variables by case. – Andreas Rossberg Jun 30 '13 at 16:50
  • @AndreasRossberg: *The Standard ML Basis Library*, after describing its conventions for letter-case in value identifiers (page 3), states that compilers could use these conventions to detect this exact mistake and issue a warning for it. It's too bad that compilers don't seem to have taken the hint! – ruakh Jul 01 '13 at 07:08
  • @ruakh, yes, though there is at least one compiler (Alice ML) that issues warnings about naming convention violations -- which I know because I wrote its front-end. :) – Andreas Rossberg Jul 01 '13 at 07:14