Несколько выходов из функции F # - PullRequest
12 голосов
/ 23 октября 2009

Я мог бы сделать это легко в 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? failwith мой единственный вариант здесь?

Это то, что я до сих пор получил, и все нормально:

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

Есть ли способ ФП справиться с подобными вещами?

Ответы [ 5 ]

10 голосов
/ 23 октября 2009

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

Итак, это 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.

5 голосов
/ 23 октября 2009

По моему мнению, выражения соответствия являются аналогом 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. На самом деле нет предела тому, как маленькие функции должны быть на функциональном языке.

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

4 голосов
/ 23 октября 2009

Прежде всего, как уже отмечали другие, это не «путь 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, остальная часть блока даже не оценивается. Если блок течет «с конца» без return, вы получите исключение времени выполнения (пока я не вижу способа применить это во время компиляции).

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

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

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

2 голосов
/ 31 января 2014

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

Поскольку выражение последовательности вычисляется лениво, это означает, что оно будет продолжаться только до точки первой ошибки (при условии, что вы никогда не вызываете ничего, кроме 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
1 голос
/ 23 октября 2009

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

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