Несколько Выходов Из Функции F#

я мог бы сделать это легко в C++ (примечание :Я не проверял это на правильность-это только для иллюстрации того, что я пытаюсь сделать):

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

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

      //normal processing

      return Success;
   }

но я не могу понять, как выйти из рутины в начале F#. То, что я хочу сделать, это выйти из функции на плохом входе, но продолжить, если вход в порядке. Я упускаю какое-то фундаментальное свойство F# или я подхожу к проблеме неправильно, так как я только изучаю FP? Это произойдет сбой мой единственный вариант здесь?

это то, что у меня есть до сих пор, и он компилируется 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;;

есть ли способ FP справиться с такого рода вещами?

5 ответов


на мой взгляд, выражения соответствия являются аналогом F# раннего выхода для вызова ошибочных условий и обработки их отдельно. Для вашего примера я бы написал:

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

это хорошо отделяет случай ошибки. В общем, если вам нужно выйти из середины чего-то, разделите функции, а затем поместите случай ошибки в match. На самом деле нет предела тому, насколько маленькие функции должны быть в функциональном языке.

кроме того, использование дискриминации объединения как наборы целочисленных констант немного странные. Если вам нравится эта идиома, имейте в виду, что вам не нужно включать имя типа при обращении к ним.


в F# все состоит из выражений (в то время как во многих других языках ключевым строительным блоком является оператор). Нет способа выйти из функции рано, но часто это не требуется. В C, у вас есть if/else блоки, где ветви состоят из операторов. В F# есть if/else выражение, где каждая ветвь оценивает значение некоторого типа и значение всего if/else expression-это значение одной ветви или другой.

так это C++:

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

выглядит так В F#:

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

ваш код находится на правильном пути, но вы можете переделать его, положив большую часть твоей логике в else ветка, с логикой "раннего возврата" в if филиала.


прежде всего, как уже отмечали другие, это не "Путь F#" (ну, не путь FP, на самом деле). Поскольку вы имеете дело не с утверждениями, а только с выражениями, на самом деле нет ничего, из чего можно вырваться. В общем случае это обрабатывается вложенной цепочкой if..then..else заявления.

тем не менее, я, конечно, вижу, где есть достаточно потенциальных точек выхода, что долго if..then..else цепь может быть не очень читаемой-особенно при работе с некоторым внешним API, который написан, чтобы возвращать коды ошибок, а не создавать исключения при сбоях (например, Win32 API или какой-то COM-компонент), поэтому вам действительно нужен этот код обработки ошибок. Если это так, то, похоже, способ сделать это в F#, в частности, написать процесс для него. Вот мой первый взгляд на это:

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

пример использования:

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

как вы можете видеть, он эффективно определяет все конструкции таким образом, что, как только return is обнаруженный, остальная часть блока даже не оценивается. Если блок течет "с конца" без return, вы получите исключение во время выполнения (я не вижу никакого способа осуществить это во время компиляции до сих пор).

это поставляется с некоторыми ограничениями. Прежде всего, рабочий процесс действительно не завершен - он позволяет использовать let, use, if, while и for внутри, но не try..with или try..finally. Это можно сделать - нужно реализовать Block.TryWith и Block.TryFinally - но я не могу найти документы для них до сих пор, поэтому для этого потребуется немного угадать и больше времени. Я могу вернуться к этому позже, когда у меня будет больше времени, и добавить их.

во-вторых, поскольку рабочие процессы - это просто синтаксический сахар для цепочки вызовов функций и лямбд - и, в частности, весь ваш код находится в лямбдах-вы не можете использовать let mutable внутри рабочего процесса. Вот почему я использовал ref и ! в приведенном выше примере кода, который является универсальным обход.

наконец, есть неизбежный штраф за производительность из-за всех вызовов лямбда. Предположительно, F# лучше оптимизирует такие вещи, чем, скажем, C# (который просто оставляет все, как есть в IL), и может встроить материал на уровне IL и делать другие трюки; но я мало знаю об этом, поэтому точная производительность, если она есть, может быть определена только профилированием.


опция, похожая на Павла, но без необходимости вашего собственного workflow builder, просто поместить блок кода в seq выражение, и она yield сообщения об ошибках. Затем сразу после выражения вы просто вызываете FirstOrDefault чтобы получить первое сообщение об ошибке (или null).

поскольку выражение последовательности оценивает лениво, это означает, что оно будет продолжаться только до точки первой ошибки (предполагая, что вы никогда не вызываете ничего, кроме FirstOrDefault на последовательность). А если нет ошибка тогда просто проходит до конца. Поэтому, если вы сделаете это таким образом, Вы сможете думать о yield так же, как раннее возвращение.

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

эта рекурсивная функция Фибоначчи имеет две точки выхода:

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