Excepciones
Todos los lenguajes de programación proveen alguna forma de trabajar con errores inesperados. Ya sea un archivo que no existe, una página web que no se puede alcanzar, una división por cero, todos estos son errores que a priori podrían suceder y que el desarrollador debiera considerar al momento del diseño del código. No hay experiencia de usuario más exasperante que un programa cerrándose solo sin ninguna explicación o posibilidad de intervención del usuario.
Si bien más adelante veremos algunas formas más adecuadas de situaciones de errores, la forma tradicional es a través del manejo de excepciones. La excepción es la señal de una situación anómala que interrumpe el flujo normal del programa. En este caso, se procura capturar la excepción y proceder en consecuencia.
let getHead (l: int list) =
l.Head
getHead []
System.InvalidOperationException: The input list was empty.
at Microsoft.FSharp.Collections.FSharpList`1.get_Head() in D:\a\_work\1\s\src\FSharp.Core\prim-types.fs:line 4072
at <StartupCode$FSI_0016>.$FSI_0016.main@()
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
En este caso el sistema emite la excepción InvalidOperationException
, y nos dice la causa The input list was empty
.
Cómo capturamos la excepción? Con un bloque try...with
:
let getHeadExn (l: int list) =
try
l.Head
with
| :? System.InvalidOperationException ->
printfn "Exception!"
-1
getHeadExn []
Exception!
-1
Nuevamente F# utiliza pattern matching para verificar el tipo de excepción, a través del símbolo :?
. El bloque try...with
no deja de ser una expresión, por lo tanto ambas ramas (try
y with
) tienen que retornar el mismo tipo de dato. De allí el valor -1
en la rama with
, para indicar que no es posible obtener el valor Head
de la lista.
Es posible obtener más datos de la excepción, de la siguiente forma:
let getHeadExnCustomMsg (l: int list) =
try
l.Head
with
| :? System.InvalidOperationException as ex ->
printfn "Exception!: %A" (ex.Message)
-1
getHeadExnCustomMsg []
Exception!: "The input list was empty."
-1
Se pueden crear excepciones en lugar de utilizar las del sistema:
exception ExceptionForList of string
let getHeadExn2 (l: int list) =
try
match l with
| [] -> raise (ExceptionForList "List is empty")
| _ -> l.Head
with
| ExceptionForList s ->
printfn "%s" s
-1
getHeadExn2 []
List is empty
-1
En este caso es el programador que decide emitir una excepción personalizada, a través de la función raise
.
exception DivideByOne of string
let divide x y =
match y with
| 0 -> invalidOp "Trying to divide by zero!!"
| 1 -> raise (DivideByOne "Can't believe you are trying to divide by one")
| _ -> x/y
printfn "%A" (divide 4 2)
2
printfn "%A" (divide 4 1)
FSI_0020+DivideByOne: DivideByOne "Can't believe you are trying to divide by one"
at FSI_0020.divide(Int32 x, Int32 y)
at <StartupCode$FSI_0021>.$FSI_0021.main@()
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
printfn "%A" (divide 4 0)
System.InvalidOperationException: Trying to divide by zero!!
at FSI_0020.divide(Int32 x, Int32 y)
at <StartupCode$FSI_0022>.$FSI_0022.main@()
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
let annoyedDivide x y =
try
divide x y
with
| DivideByOne s ->
printfn "%A" (s)
-1
| :? System.InvalidOperationException as ex ->
printfn "Message: %A" (ex.Message)
-1
annoyedDivide 4 2
2
annoyedDivide 4 1
"Can't believe you are trying to divide by one"
-1
annoyedDivide 4 0
Message: "Trying to divide by zero!!"
-1
La función annoyedDivide
captura las excepciones, y en esos casos devuelve -1
.
Obsérvese que es cuanto menos discutible que una función que divide retorne -1 como un valor en caso de error, puesto que -1 es efectivamente un valor válido al hacer una división. Si bien el programa continuará corriendo dado que se capturó la excepción, el problema ahora lo tiene la función que llama a
annoyedDivide
, ya que tiene que distinguir entre el caso de -1 como valor válido o -1 como signo de error. Veremos más adelante una forma robusta de proceder en este caso.