2012-06-30 20 views
5

OCaml try .. with no ofrece una cláusula finally como Java. Sin embargo, sería útil, especialmente cuando se trata de efectos secundarios. Por ejemplo, me gusta abrir un archivo, pasar el archivo abierto a una función y cerrarlo. En caso de que la función genere una excepción, tengo que atraparla para poder cerrar el archivo. Esto se vuelve cada vez más complicado cuando se abren múltiples archivos y la apertura en sí misma también puede fallar. ¿Existe un patrón de programación establecido para lidiar con esto?Emulación try-with-finally en OCaml

A continuación se muestra una función simple que ilustra el problema. La función f se aplica a un canal que pertenece a un archivo si se proporciona path y stdin en caso contrario. Como no hay una cláusula finally, close_in io aparece dos veces.

let process f = function 
    | Some path -> 
     let io = open_in path in 
      ((try f io with exn -> close_in io; raise exn) 
      ; close_in io 
      ) 
    | None -> f stdin 

Respuesta

6

¿Hay un patrón de programación establecida para hacer frente a esto?

Sí, funciones de contenedor que desacoplan la limpieza de recursos del manejo de excepciones. Lo que hago es utilizar un envoltorio genérico, unwind (un LISPism con la que estoy más acostumbrado):

let unwind ~(protect:'a -> unit) f x = 
    try let y = f x in protect x; y 
    with e -> protect x; raise e 

Se trata de un simple envoltorio que no lo hace correctamente dar cuenta de las excepciones planteadas en protect; un envoltorio totalmente comprobado que asegura que protect se llama una sola vez, aunque él mismo no podría ser Yaron Minski's, o este otro que creo que es un poco más claro:

let unwind ~protect f x = 
    let module E = struct type 'a t = Left of 'a | Right of exn end in 
    let res = try E.Left (f x) with e -> E.Right e in 
    let() = protect x in 
    match res with 
    | E.Left y -> y 
    | E.Right e -> raise e 

Entonces, defino casos específicos según sea necesario, por ejemplo, :

let with_input_channel inch f = 
    unwind ~protect:close_in f inch 

let with_output_channel otch f = 
    unwind ~protect:close_out f otch 

let with_input_file fname = 
    with_input_channel (open_in fname) 

let with_output_file fname = 
    with_output_channel (open_out fname) 

La razón me cambio de los parámetros de las funciones específicas with_ es que me resulta más conveniente para la programación de orden superior; en particular, mediante la definición de un operador a la aplicación Haskell, puedo escribir:

let() = with_output_file "foo.txt" $ fun otch -> 
    output_string otch "hello, world"; 
    (* ... *) 

con una sintaxis no es muy pesado. Para un ejemplo algo más complicado, considere lo siguiente:

let with_open_graph spec (proc : int -> int -> unit) = 
    unwind ~protect:Graphics.close_graph (fun() -> 
    proc (Graphics.size_x()) (Graphics.size_y()); 
    ignore (Graphics.wait_next_event [Graphics.Button_down]); 
    ignore (Graphics.wait_next_event [Graphics.Button_up])) 
    (Graphics.open_graph spec) 

que se puede utilizar con una llamada como with_open_graph " 400x300" $ fun width height -> (*...*).

+2

¡Muy bonito! Un poco a un lado: Supongo que '$' se define como 'let ($) f x = f x' pero esto lo haría asociativo izquierdo mientras que es correcto asociativo en Haskell. En consecuencia, 'print_int $ (+) 3 $ 4' no funciona en OCaml. Para un operador de aplicación asociativo correcto, se podría definir 'let (@@) f x = f x'. –

+0

Si 'protect' genera una excepción, ¿se ejecuta dos veces? Porque la excepción se detecta y 'protect' se ejecuta nuevamente. Véase también una [aplicación de Yaron Minsky de 'protect'] (http://caml.inria.fr/pub/ml-archives/caml-list/2003/07/5ff669a9d2be35ec585b536e2e0fc7ca.en.html). –

+1

@ChristianLindig: Sí, mi código original no maneja excepciones dobles. Ver mi edición para una versión "funcional" que creo que mejora en la de Yaron. –

2

No está incorporado a OCaml por lo que yo sé, pero se puede escribir una biblioteca que le permite codificar dichos patrones. Una biblioteca de ejemplo es Catch me if you can, que es una codificación monádica de errores. Here es un tutorial para agregar una construcción finally mediante metaprogramación. Probablemente haya otros enfoques también.

+0

Gracias por las sugerencias. Esperaba una solución inteligente dentro del lenguaje central en lugar de confiar en Camlp5. –

1

Aquí hay una opción. (He quitado la coincidencia en path a hervir el código abajo a un ejemplo mínimo.)

let process f path = 
    let exn = ref None in 
    let io = open_in path in 
    (try f io with e -> exn := Some e); 
    close_in io; 
    match !exn with Some e -> raise e | None ->() 
+0

La idea aquí es almacenar la excepción para evitar la duplicación de código que entraría en un bloque final. Un diseño alternativo sería capturar el bloque final en una función local y llamarlo desde dos lugares. –

3

Del libro Unix system programming in OCaml por Xavier Leroy y Didier Rémy en el capítulo Generalities

"No hay incorporado de finalización construcción TRY ... finalizar en el lenguaje OCaml, pero puede ser fácilmente definido:"

let try_finalize f x finally y = 
    let res = try f x with exn -> finally y; raise exn in 
    finally y; 
    res 
1

La colección de la biblioteca Baterías OCaml ofrece dos funciones que se pueden utilizar para finalmente

http://ocaml-batteries-team.github.io/batteries-included/hdoc/BatPervasives.html

val finally : (unit -> unit) -> ('a -> 'b) -> 'a -> 'b 

finally fend f x llama f x y luego fend() incluso si f x una excepción.

val with_dispose : dispose:('a -> unit) -> ('a -> 'b) -> 'a -> 'b 

with_dispose dispose f x invoca f en x, llamando dispose x cuando f termina (ya sea con un valor de retorno o una excepción).

1

Si tanto la función y el bloque finally genera una excepción Yo prefiero ver la excepción inicial, por lo que usar algo como esto:

type 'a result = OK of 'a | Exn of exn 
let result_of f x = try OK (f x) with e -> Exn e 

(** invokes [f x], and always releases [x] by invoking [release x] *) 
let do_with x release f = 
    let result = result_of f x in 
    let closed = result_of release x in 
    match result, closed with 
    | Exn e, _ -> raise e (* [f x] raised exception *) 
    | _, Exn e -> raise e (* [release x] raised exception *) 
    | OK r, OK() -> r (* all OK *)