2009-10-22 21 views
9

que podría hacer esto fácilmente en C++ (nota: No he probado esto para la corrección - que es sólo para ilustrar lo que estoy tratando de hacer):múltiples salidas de F # Función

const int BadParam = -1; 
    const int Success = 0; 

    int MyFunc(int param) 
    { 
     if(param < 0) 
     { 
     return BadParam; 
     } 

     //normal processing 

     return Success; 
    } 

Pero no puede entender cómo salir de una rutina temprano en 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 estoy abordando el problema de forma incorrecta ya que estoy aprendiendo FP? ¿Es failwith mi única opción aquí?

Esto es lo que tengo hasta ahora y compila bien:

#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 una manera de FP de tratar con este tipo de cosas?

+0

Es probable que desee algún tipo de continuación de escape, pero no tengo idea de cómo sería en F #. – leppie

+0

http://stackoverflow.com/questions/833180/handy-f-snippets/1475822#1475822 – Benjol

+0

@Benjol, gracias - exactamente el tipo de cosa que estaba buscando. –

Respuesta

4

En mi opinión, expresiones de coincidencia son el F # analógica de salida temprana para llamar a cabo condiciones erróneas y gastos de envío por separado. Para su 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 muy bien el caso de error. En general, si necesita salir del centro de algo, divida las funciones y luego coloque el caso de error en match. Realmente no hay límite para cómo deben ser las funciones pequeñas en un lenguaje funcional.

Como un lado, su uso de uniones discriminadas como conjuntos de constantes enteras es un poco extraño. Si le gusta ese modismo, tenga en cuenta que no necesita incluir el nombre del tipo al referirse a ellos.

+0

Nathan, Gracias - ese último comentario es algo interesante porque lo intenté sin el nombre del tipo y falló. Solo funcionó con el nombre de tipo especificado. –

+0

Extraño. Cuando usa una unión discriminada como una enumeración, requiere el espacio de nombre completo. En su situación (no coincide con el patrón en los tipos), solo declararía un conjunto de constantes enteras. –

+0

@Nathan Sanders, en realidad eso es lo que hice inicialmente: un conjunto de constantes enteras. Pero parecían más un conjunto enumerado de posibles retornos que una serie de constantes. 6 de una, 1/2 docena de otra, supongo. Aún así, es bueno saber lo que no se considera idiomático cuando uno está tratando de aprender un nuevo idioma. –

7

En F #, todo está compuesto de expresiones (mientras que en muchos otros idiomas, el elemento clave es una afirmación). No hay forma de salir de una función antes, pero a menudo esto no es necesario. En C, tiene un if/else bloques donde las ramas se componen de instrucciones. En F #, hay una expresión if/else, donde cada rama evalúa a un valor de algún tipo, y el valor de la expresión completa if/else es el valor de una rama u otra.

Así que este C++:

int func(int param) { 
    if (param<0) 
    return BadParam; 
    return Success; 
} 

es la siguiente con F #:

let func param = 
    if (param<0) then 
    BadParam 
    else 
    Success 

Su código se encuentra en el camino correcto, pero se puede refactorizar, poniendo la mayor parte de su lógica en el else rama, con la lógica de "retorno anticipado" en la rama if.

+0

@kvb Gracias por la respuesta. Creo que probablemente has alcanzado un muy buen enfoque. –

+1

Por supuesto, hay una forma de múltiples puntos de salida: usar una continuación de salida en el estilo de paso de continuación - ¡Muy común en Haskell y también posible en F #! – Dario

1

Esta función recursiva de Fibonacci tiene dos puntos de salida:

let rec fib n = 
    if n < 2 then 1 else fib (n-2) + fib(n-1);; 
       ^ ^
+0

Ese es un punto interesante, Robert. No estoy muy seguro de cómo traducir esa pregunta, pero es un buen punto. –

+0

Edité mi respuesta para mostrar los puntos de salida. El punto de salida más a la izquierda es el caso excepcional; proporciona una ruta de salida de la recursión. –

4

En primer lugar, como ya han señalado otros, no es "la manera F #" (bueno, no la forma FP, en realidad). Como no se trata de declaraciones, sino de expresiones, no hay nada de qué salir. En general, esto se trata con una cadena anidada de if .. then .. else sentencias.

Dicho esto, ciertamente puedo ver dónde hay suficientes puntos de salida potenciales que un largo if .. then ..else cadena no puede ser muy legible, especialmente cuando se trata de API externa escrita para devolver códigos de error en lugar de lanzar excepciones en fallas (por ejemplo API Win32 o algún componente COM), por lo que realmente necesita ese código de manejo de errores. Si es así, parece que la manera de hacer esto en F # en particular sería escribir un workflow para ello. Aquí es mi primera toma en ella: muestra

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

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 se puede ver, se define de forma eficaz todas las construcciones, de tal manera que, tan pronto como return se encuentra, el resto del bloque ni siquiera es evaluado. Si el bloque fluye "fuera del final" sin un return, obtendrá una excepción de tiempo de ejecución (no veo ninguna forma de imponer esto en tiempo de compilación hasta el momento).

Esto viene con algunas limitaciones. En primer lugar, el flujo de trabajo realmente no es completa - que le permite utilizar let, use, if, while y for dentro, pero no try .. with o try .. finally. Se puede hacer, debe implementar Block.TryWith y Block.TryFinally, pero no puedo encontrar los documentos para ellos hasta el momento, por lo que necesitará un poco de adivinanzas y más tiempo. Podría volver más tarde cuando tenga más tiempo y agregarlos.

En segundo lugar, como los flujos de trabajo son solo azúcar sintáctica para una cadena de llamadas a función 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, existe la penalización de rendimiento inevitable debido a todas las llamadas lambda. Supuestamente, F # es mejor para optimizar tales cosas que, digamos C# (que simplemente deja todo como está en IL), y puede alinear cosas en el nivel IL y hacer otros trucos; pero no sé mucho al respecto, por lo que el rendimiento exacto alcanzado, si lo hubiera, solo podría determinarse mediante el perfil.

+0

@Pavel Minaev, espero que pronto comprenda F # lo suficientemente bien como para entender su respuesta. :-) Gracias por tomarse el tiempo para responder a mi pregunta. –

+0

Puede usar una expresión de secuencia para hacer esencialmente lo mismo, sin la necesidad de crear un nuevo constructor. –

+0

¿La secuencia vuelve? Pensé que solo tiene rendimiento. –

1

Una opción similar a la de Pavel, pero sin necesidad de su propio generador de flujo de trabajo, es solo poner su bloque de código dentro de una expresión seq y tener los mensajes de error yield. Luego, justo después de la expresión, simplemente llame al FirstOrDefault para obtener el primer mensaje de error (o nulo).

Dado que una expresión de secuencia se evalúa de forma diferida, eso significa que solo pasará al punto del primer error (suponiendo que nunca llame a nada más que FirstOrDefault en la secuencia). Y si no hay ningún error, simplemente se extiende hasta el final. Entonces, si lo haces de esta manera, podrás pensar en yield como si fuera un regreso 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 
Cuestiones relacionadas