2009-10-22 11 views
9

ich dies leicht in C tun könnte ++ (Anmerkung: Ich habe das nicht für Richtigkeit prüfen - es ist nur zu veranschaulichen, was ich versuche zu tun):Mehrere Exits Von F # Funktion

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

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

     //normal processing 

     return Success; 
    } 

Aber ich kann nicht herausfinden, wie man eine Routine in F # vorzeitig beendet. Was ich tun möchte, ist die Funktion bei einer schlechten Eingabe zu verlassen, aber weiter, wenn die Eingabe in Ordnung ist. Fehle ich eine fundamentale Eigenschaft von F # oder gehe ich das Problem auf die falsche Art an, da ich gerade FP lerne? Ist ein failwith meine einzige Option hier?

Das ist, was ich so weit gekommen und es kompiliert ok:

#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;; 

Gibt es eine FP Art und Weise mit dieser Art der Sache umzugehen?

+0

Sie wollen wahrscheinlich eine Art Fluchtfortsetzung, aber ich habe keine Ahnung, wie es in F # wäre. – leppie

+0

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

+0

@Benjol, danke - genau die Art von was ich suchte. –

Antwort

4

Meiner Meinung nach, sind Spiel Ausdrücke die F # analog früh Ausgang für fehlerhafte Bedingungen zu rufen und sie getrennt behandeln. Für Ihr Beispiel würde ich schreiben:

[<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 

Dies trennt den Fehlerfall schön. Im Allgemeinen, wenn Sie aus der Mitte von etwas beenden müssen, teilen Sie Funktionen und dann den Fehlerfall in eine match. Es gibt wirklich keine Grenzen wie kleine Funktionen in einer funktionalen Sprache sein sollten.

Nebenbei ist Ihre Verwendung von diskriminierten Gewerkschaften als Sätze von ganzzahligen Konstanten ein wenig seltsam. Wenn Sie dieses Idiom mögen, beachten Sie, dass Sie den Typnamen nicht angeben müssen, wenn Sie darauf verweisen.

+0

Nathan, Danke - dieser letzte Kommentar ist irgendwie interessant, weil ich das ohne den Typnamen versucht habe und es gescheitert ist. Arbeitet nur mit dem angegebenen Typnamen. –

+0

Seltsam. Wenn Sie eine diskriminierte Union wie eine Enumeration verwenden, erfordert sie den vollständigen Namespace. In Ihrer Situation (Sie Muster nicht übereinstimmen auf den Typen), würde ich nur eine Reihe von Integer-Konstanten deklarieren. –

+0

@Nathan Sanders, eigentlich habe ich das ursprünglich gemacht - eine Menge von Integer-Konstanten. Aber sie schienen mehr wie eine aufgezählte Reihe möglicher Rückgaben als eine Reihe von Konstanten zu sein. 6 von einem, 1/2 Dutzend eines anderen schätze ich. Dennoch ist es gut zu wissen, was nicht als idiomatisch gilt, wenn man versucht, eine neue Sprache zu lernen. –

7

In F # besteht alles aus Ausdrücken (während in vielen anderen Sprachen der Schlüsselbaustein eine Anweisung ist). Es gibt keine Möglichkeit, eine Funktion vorzeitig zu beenden, aber oft wird dies nicht benötigt. In C haben Sie eine if/else Blöcke, wo die Zweige aus Aussagen bestehen. In F # gibt es einen if/else Ausdruck, wobei jeder Zweig einen Wert von einem Typ auswertet und der Wert des gesamten Ausdrucks if/else der Wert des einen oder anderen Zweigs ist.

Also das C++:

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

Sieht aus wie dies in F #:

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

Ihr Code auf dem richtigen Weg ist, aber man kann es Refactoring, in der else die meisten Ihrer Logik setzen Verzweigung mit der Logik "frühe Rückgabe" im Zweig if.

+0

@ kvb Danke für die Antwort. Ich denke, Sie haben wahrscheinlich einen sehr guten Ansatz gefunden. –

+1

Natürlich gibt es einen Weg von mehreren Ausstiegspunkten: Mit einer Exit-Fortsetzung in Fortsetzungs-Passing-Stil - Sehr häufig in Haskell und auch möglich in F #! – Dario

1

Diese rekursive Fibonacci-Funktion hat zwei Ausgangspunkte:

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

Das ist ein interessanter Punkt, Robert. Ich bin mir nicht ganz sicher, wie ich diese Frage übersetzen soll, aber es ist ein guter Punkt. –

+0

Ich habe meine Antwort bearbeitet, um die Ausgangspunkte anzuzeigen. Der äußerste linke Austrittspunkt ist der Ausnahmefall; Es bietet einen Weg aus der Rekursion. –

4

Zunächst einmal, wie andere bereits bemerkt haben, ist es nicht "die F # -Way" (na ja, nicht FP Weg, wirklich). Da Sie sich nicht mit Aussagen, sondern nur mit Ausdrücken beschäftigen, gibt es nicht wirklich etwas, aus dem man ausbrechen könnte. Im Allgemeinen wird dies durch eine verschachtelte Kette von if .. then .. else Aussagen behandelt.

Das gesagt, ich kann sicherlich sehen, wo es genug potenzielle Ausgänge gibt, die eine lange if .. then ..else Kette kann nicht sehr lesbar sein - besonders, wenn es um einige externe API geht, die geschrieben wird, um Fehlercodes zurückzugeben, anstatt Ausnahmen bei Fehlern (sagen Win32 API oder eine COM-Komponente), so dass Sie wirklich diese Fehlerbehandlung Code benötigen. Wenn dem so ist, scheint es der Weg, dies insbesondere in F # zu tun, ein workflow dafür zu schreiben. Hier ist meine erste Klappe daran:

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

Nutzungs Beispiel:

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 

Wie Sie sehen können, ist es definiert effektiv alle Konstrukte in der Weise, dass, sobald return angetroffen wird, der Rest der Block wird nicht einmal ausgewertet. Wenn Block "ohne Ende" ohne return fließt, erhalten Sie eine Laufzeitausnahme (ich sehe keine Möglichkeit, dies bis zur Kompilierung zu erzwingen).

Dies ist mit einigen Einschränkungen verbunden. Zunächst einmal ist der Workflow wirklich nicht abgeschlossen - es können Sie verwenden let, use, if, while und for nach innen, aber nicht try .. with oder try .. finally. Es kann getan werden - Sie müssen Block.TryWith und Block.TryFinally implementieren - aber ich kann die Dokumente für sie so weit nicht finden, so dass dies ein wenig raten und mehr Zeit benötigen wird. Ich könnte später darauf zurückkommen, wenn ich mehr Zeit habe, und füge sie hinzu.

Zweitens, da Workflows wirklich nur syntaktischer Zucker für eine Kette von Funktionsaufrufen und Lambdas sind - und insbesondere all Ihr Code in Lambdas ist - können Sie let mutable nicht innerhalb des Workflows verwenden. Deshalb habe ich im obigen Beispielcode ref und ! verwendet, was die allgemeine Problemumgehung darstellt.

Schließlich gibt es die unvermeidliche Leistungseinbuße wegen aller Lambda-Aufrufe. Angeblich ist F # besser in der Optimierung solcher Dinge als, sagen wir C# (was einfach alles so belässt, wie es in IL ist), und kann Inline-Sachen auf IL-Ebene machen und andere Tricks machen; aber ich weiß nicht viel darüber, also konnte die genaue Performance, wenn überhaupt, nur durch Profiling bestimmt werden.

+0

@Pavel Minaev, ich hoffe, dass ich bald verstehe F gut genug, um Ihre Antwort zu verstehen. :-) Danke, dass du dir die Zeit genommen hast, meine Frage zu beantworten. –

+0

Sie könnten einen Sequenzausdruck verwenden, um im Wesentlichen dasselbe zu tun, ohne dass Sie einen neuen Builder erstellen müssen. –

+0

Führt die Sequenz zurück? Ich dachte, es hat nur Ertrag. –

1

Eine Option ähnlich wie bei Pavel, aber ohne Ihren eigenen Workflow-Builder, ist nur, um Ihren Code-Block innerhalb eines seq Ausdruck zu setzen, und es yield Fehlermeldungen haben. Dann rufen Sie einfach nach dem Ausdruck FirstOrDefault, um die erste Fehlermeldung (oder null) zu erhalten.

Da ein Sequenzausdruck träge ausgewertet wird, bedeutet dies, dass er nur bis zum ersten Fehler fortfährt (vorausgesetzt, Sie rufen nie irgendetwas außer FirstOrDefault in der Sequenz an). Und wenn es keinen Fehler gibt, läuft es einfach bis zum Ende durch. Also, wenn Sie es auf diese Weise tun, können Sie an yield wie eine frühe Rückkehr denken.

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