F#函数的多次退出

问题描述 投票:12回答:6

我可以在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;;

是否有使用FP的方法来处理这种事情?

f# structure idioms
6个回答
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中。函数语言中的小函数应该没有任何限制。

顺便说一句,您将区分联合用作整数常量集有点奇怪。如果您喜欢这种惯用语,请注意,在引用它们时不需要包括类型名称。


11
投票

在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分支中。


4
投票

首先,正如其他人已经指出的那样,它不是“ F#方式”(嗯,不是FP方式)。由于您不处理语句,而仅处理表达式,因此实际上没有什么需要突破的。通常,这由if ...then.. else语句的嵌套链处理。

就是说,我当然可以看到在哪里有足够的潜在出口点,因此长if ..then.. else链可能不太可读-尤其是在处理一些写入到外部API的情况下。返回错误代码,而不是在失败时抛出异常(例如Win32 API或某些COM组件),因此您确实需要该错误处理代码。如果是这样,似乎在F#中尤其要这样做,就是为此写一个workflow。这是我的第一个建议:

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”的代码块“从头到尾”流动,您将获得运行时异常(到目前为止,我看不到有任何方法可以强制执行此操作)。

这有一些限制。首先,工作流确实还不完整-它允许您在内部使用letuseifwhilefor,但不能使用try .. with或[C0 ] .. try。可以完成-您需要实现finallyBlock.TryWith-但到目前为止我还找不到它们的文档,因此这将需要一点猜测和更多时间。当我有更多时间时,我可能会再回来,并添加它们。

[其次,由于工作流实际上只是函数调用和lambda链的语法糖-特别是所有代码都在lambda中-您不能在工作流内部使用Block.TryFinally。这就是为什么我在上面的示例代码中使用了let mutableref,这是通用的解决方法。

最后,由于所有的lambda调用,不可避免地会有性能损失。据推测,F#比C#(后者只保留IL中的所有内容)更擅长优化此类事情,并且可以在IL级别内嵌东西并做其他技巧。但是我对此了解不多,因此确切的性能影响(如果有的话)只能通过性能分析来确定。


2
投票

[类似于Pavel的选项,但不需要您自己的工作流生成器,只是将您的代码块放在!表达式中,并显示seq错误消息。然后,在表达式之后,您只需调用yield以获取第一条错误消息(或为null)。

由于序列表达式的计算是延迟的,这意味着它只会继续执行第一个错误的点(假设您只对序列中的FirstOrDefault调用任何东西)。如果没有错误,那么它将一直持续到最后。因此,如果您以这种方式进行操作,就可以像早日返回一样思考FirstOrDefault

yield

1
投票

此递归斐波那契函数有两个出口点:

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

0
投票

我为自己发现了以下方法。基本上,它生成可能出口的let rec fib n = if n < 2 then 1 else fib (n-2) + fib(n-1);; ^ ^ ,其中每个出口由seq生成,然后取seq的yield仅返回首先计算的出口值。我不确定我们是否可以在某种程度上称这种方法“有效”。同样不确定这种方法的效率如何,以及如何在后台使用语言惰性。同样不确定多年后作者是否有需要。但是这种方式的一些优点是,代码看起来很像最初是在非功能性语言中看到的,而只使用了最少的一组固有功能。

请参阅下面的问题的第一个示例代码:

Seq.head

让我们进行一些测试呼叫:

let BadParam : int = -1
let Success : int = 0

let MyFunc param =
    seq {
        if (param < 0) then
           printfn " bad param detected "
           yield BadParam

        // normal processing
        printfn "normal processing"
        yield Success
     } |> Seq.head

让我们查看输出:

printfn "%i" (MyFunc 11)
printfn "%i" (MyFunc -11)

希望这个想法对可能对此有所困扰的人有所帮助。欢迎您就上述我的问题发表任何评论。

© www.soinside.com 2019 - 2024. All rights reserved.