Múltiples salidas de la función F #
Pregunta
Podría hacer esto fácilmente en C ++ (nota: no lo probé para ver si está correcto, es solo para ilustrar lo que estoy tratando de hacer):
const int BadParam = -1;
const int Success = 0;
int MyFunc(int param)
{
if(param < 0)
{
return BadParam;
}
//normal processing
return Success;
}
Pero no puedo averiguar cómo salir de una rutina al principio de F #. Lo que quiero hacer es salir de la función en una entrada incorrecta pero continuar si la entrada es correcta. ¿Me estoy perdiendo alguna propiedad fundamental de F # o me estoy acercando al problema de forma incorrecta ya que estoy aprendiendo FP? Es un failwith mi única opción aquí?
Esto es lo que tengo hasta ahora y se compila:
#light
module test1
(* Define how many arguments we're expecting *)
let maxArgs = 2;;
(* The indices of the various arguments on the command line *)
type ProgArguments =
| SearchString = 0
| FileSpec = 1;;
(* Various errorlevels which the app can return and what they indicate *)
type ProgReturn =
| Success = 0
| WrongNumberOfArgumentsPassed = 1;;
[<EntryPoint>]
let main (args:string[]) =
printfn "args.Length is %d" args.Length
let ProgExitCode = if args.Length <> maxArgs then
printfn "Two arguments must be passed"
int ProgReturn.WrongNumberOfArgumentsPassed
(* Want to exit "main" here but how? *)
else
int ProgReturn.Success
let searchstring, filespec = args.[int ProgArguments.SearchString],args.[int ProgArguments.FileSpec];
printfn "searchstring is %s" searchstring
printfn "filespec is %s" filespec
ProgExitCode;;
¿Hay alguna manera de tratar este tipo de cosas con la FP?
Solución
En mi opinión, las expresiones de coincidencia son el análogo de F # de salida temprana para indicar condiciones erróneas y manejarlas por separado. Para tu ejemplo, escribiría:
[<EntryPoint>]
let main (args:string[]) =
printfn "args.Length is %d" args.Length
match args with
| [| searchstring; filespace |] ->
// much code here ...
int Success
| _ -> printfn "Two arguments must be passed"
int WrongNumberOfArgumentsPassed
Esto separa el caso de error muy bien. En general, si necesita salir de la mitad de algo, dividir las funciones y luego colocar el caso de error en una coincidencia
. Realmente no hay límite a cómo deben ser las funciones pequeñas en un lenguaje funcional.
Como nota aparte, su uso de uniones discriminadas como conjuntos de constantes enteras es un poco extraño. Si le gusta ese idioma, tenga en cuenta que no necesita incluir el nombre de tipo cuando se refiera a ellos.
Otros consejos
En F #, todo está formado por expresiones (mientras que en muchos otros idiomas, el bloque de construcción clave es una declaración). No hay manera de salir de una función temprano, pero a menudo esto no es necesario. En C, tiene un if / else
bloques donde las sucursales están formadas por declaraciones. En F #, hay una expresión if / else
, donde cada rama se evalúa a un valor de algún tipo, y el valor de la expresión completa if / else
es el valor de uno rama o la otra.
Así que este C ++:
int func(int param) {
if (param<0)
return BadParam;
return Success;
}
Se ve así en F #:
let func param =
if (param<0) then
BadParam
else
Success
Su código está en el camino correcto, pero puede refactorizarlo, poniendo la mayor parte de su lógica en la rama else
, con el " retorno anticipado " lógica en la rama si
.
En primer lugar, como ya han dicho otros, no es " la forma F # " (bueno, no de manera FP, de verdad). Ya que no se trata de declaraciones, sino de expresiones, no hay realmente nada que romper. En general, esto se trata mediante una cadena anidada de if
.. entonces
.. else
.
Dicho esto, ciertamente puedo ver dónde hay suficientes puntos de salida potenciales que un largo if
.. luego
.. else
puede no debe ser muy legible, especialmente cuando se trata de una API externa que está escrita para devolver códigos de error en lugar de lanzar excepciones en fallas (por ejemplo, la API de Win32 o algún componente COM), por lo que realmente necesita ese código de manejo de errores. Si es así, parece que la forma de hacerlo en F # en particular sería escribir un flujo de trabajo para ello.
Aquí está mi primera toma:
type BlockFlow<'a> =
| Return of 'a
| Continue
type Block() =
member this.Zero() = Continue
member this.Return(x) = Return x
member this.Delay(f) = f
member this.Run(f) =
match f() with
| Return x -> x
| Continue -> failwith "No value returned from block"
member this.Combine(st, f) =
match st with
| Return x -> st
| Continue -> f()
member this.While(cf, df) =
if cf() then
match df() with
| Return x -> Return x
| Continue -> this.While(cf, df)
else
Continue
member this.For(xs : seq<_>, f) =
use en = xs.GetEnumerator()
let rec loop () =
if en.MoveNext() then
match f(en.Current) with
| Return x -> Return x
| Continue -> loop ()
else
Continue
loop ()
member this.Using(x, f) = use x' = x in f(x')
let block = Block()
Ejemplo de uso:
open System
open System.IO
let n =
block {
printfn "Type 'foo' to terminate with 123"
let s1 = Console.ReadLine()
if s1 = "foo" then return 123
printfn "Type 'bar' to terminate with 456"
let s2 = Console.ReadLine()
if s2 = "bar" then return 456
printfn "Copying input, type 'end' to stop, or a number to terminate with that number"
let s = ref ""
while (!s <> "end") do
s := Console.ReadLine()
let (parsed, n) = Int32.TryParse(!s)
if parsed then
printfn "Dumping numbers from 1 to %d to output.txt" n
use f = File.CreateText("output.txt") in
for i = 1 to n do
f.WriteLine(i)
return n
printfn "%s" s
}
printfn "Terminated with: %d" n
Como puede ver, define efectivamente todas las construcciones de tal manera que, tan pronto como se encuentre return
, el resto del bloque ni siquiera se evalúa. Si el bloque fluye " fuera del final " sin un return
, obtendrás una excepción de tiempo de ejecución (hasta ahora no veo ninguna forma de aplicar esto en tiempo de compilación).
Esto viene con algunas limitaciones. En primer lugar, el flujo de trabajo realmente no está completo: le permite usar let
, use
, if
, mientras que
y para
adentro, pero no intente
.. con
o intente
.. finalmente
. Se puede hacer - necesita implementar Block.TryWith
y Block.TryFinally
- pero no puedo encontrar los documentos para ellos hasta ahora, así que esto necesitará un poco. Poco de adivinar y más tiempo. Podría volver a ello más tarde, cuando tenga más tiempo, y agregarlos.
Segundo, dado que los flujos de trabajo son realmente solo azúcar sintáctica para una cadena de llamadas a funciones y lambdas, y, en particular, todo su código está en lambdas, no puede usar let mutable
dentro del flujo de trabajo. Es por eso que he usado ref
y !
en el código de ejemplo anterior, que es la solución de propósito general.
Finalmente, está la inevitable penalización de rendimiento debido a todas las llamadas de Lambda. Supuestamente, F # es mejor para optimizar tales cosas que, digamos C # (que deja todo como está en IL), y puede incluir cosas en el nivel de IL y hacer otros trucos; pero no sé mucho al respecto, por lo que el impacto exacto en el rendimiento, si lo hubiera, solo podría determinarse mediante la creación de perfiles.
Una opción similar a la de Pavel, pero sin necesitar su propio generador de flujo de trabajo, es simplemente colocar el bloque de código dentro de una expresión seq
, y hacer que produzca
mensajes de error. Luego, justo después de la expresión, simplemente llame a FirstOrDefault
para obtener el primer mensaje de error (o nulo).
Dado que una expresión de secuencia se evalúa perezosamente, eso significa que solo procederá al punto del primer error (asumiendo que nunca se llama nada excepto FirstOrDefault
en la secuencia). Y si no hay error, simplemente se ejecuta hasta el final. Por lo tanto, si lo hace de esta manera, podrá pensar en yield
como un retorno anticipado.
let x = 3.
let y = 0.
let errs = seq {
if x = 0. then yield "X is Zero"
printfn "inv x=%f" (1./x)
if y = 0. then yield "Y is Zero"
printfn "inv y=%f" (1./y)
let diff = x - y
if diff = 0. then yield "Y equals X"
printfn "inv diff=%f" (1./diff)
}
let firstErr = System.Linq.Enumerable.FirstOrDefault errs
if firstErr = null then
printfn "All Checks Passed"
else
printfn "Error %s" firstErr
Esta función de Fibonacci recursiva tiene dos puntos de salida:
let rec fib n =
if n < 2 then 1 else fib (n-2) + fib(n-1);;
^ ^