13

我可以在 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,所以我以错误的方式解决了这个问题?失败是我唯一的选择吗?

这就是我到目前为止所得到的,它可以编译:

   #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 方式?

4

7 回答 7

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

于 2009-10-22T20:58:05.687 回答
6

在我看来,匹配表达式是早期退出的 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. 函数式语言中的小函数实际上没有限制。

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

于 2009-10-22T21:22:32.807 回答
5

首先,正如其他人已经指出的那样,这不是“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遇到,甚至不评估块的其余部分。如果块在没有 a 的情况下“结束”流动return,您将获得运行时异常(到目前为止,我没有看到任何在编译时强制执行此操作的方法)。

这有一些限制。首先,工作流程确实不完整 - 它允许您使用let, use, if, whileand forinside,但不能使用try..withtry.. finally。可以做到——你需要实现Block.TryWithBlock.TryFinally——但到目前为止我找不到他们的文档,所以这需要一点猜测和更多时间。当我有更多时间时,我可能会再回来,并添加它们。

其次,由于工作流实际上只是一系列函数调用和 lambda 的语法糖——特别是,你的所有代码都在 lambda 中——你不能let mutable在工作流中使用。这就是我在上面的示例代码中使用ref的原因!,这是通用的解决方法。

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

于 2009-10-23T00:10:41.030 回答
3

一个类似于 Pavel 的选项,但不需要您自己的工作流构建器,只是将您的代码块放在一个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
于 2014-01-31T01:37:51.677 回答
1

这个递归斐波那契函数有两个退出点:

let rec fib n =
  if n < 2 then 1 else fib (n-2) + fib(n-1);;
                ^      ^
于 2009-10-22T21:01:02.527 回答
0

我为自己发现了以下方法。基本上,它会生成一个seq可能的退出,其中每个退出由 生成yield,然后Seq.head使用 seq 仅返回首先计算的退出值。我不确定我们是否可以在任何程度上将此方法称为“功能性”。也不确定这种方法的效率如何,以及语言惰性是如何在幕后使用的。也不知道这么多年后作者是否还需要。但是这种方式的一些优点是代码看起来很像它最初在非函数式语言中的样子,而只使用了最少的内在特性集。

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

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)

让我们回顾一下输出:

normal processing
0
bad param detected
-1

希望这个想法对那些可能陷入困境的人有所帮助。欢迎对我的上述问题发表任何评论。

于 2019-12-26T12:56:46.650 回答
0

我喜欢这样,从来没有提到过 F# 类型的结果?

它是由正式给出的

[<Struct>]
type Result<'success,'error> =
   | Ok of 'success
   | Error of 'error

这将满足您的需求,并在处理非 IO 错误以提高性能时使用。

一个很好的例子是解析器函数

type Parser<'token, 'ret,'error> = Parser of ('token seq -> Result<'ret * 'token seq, 'error>)

let Run (Parser p) = p

let OrParser parser1 parser2 =
    fun input ->
        match Run parser1 input with
        | Ok (item, rest) -> Ok(item, rest)
        | Error _ ->
            match Run parser2 input with
            | Ok (item, rest) -> Ok(item, rest)
            | Error msg -> Error msg
    |> Parser

如果您不熟悉 |> 运算符,则只需将左侧(或上方)代码的结果传递给右侧的参数,这里是上面的 lambda 函数

基本案例函数的实现由用户选择,只要它符合函数类型即可。您可能会注意到,它是任何可能失败的映射(函数)的高度通用函数表示。这包括所有永不失败的功能。

以上代码是我个人的一些(无代码生成)lexer/parser生成器,仅用于在lexer和parser中构造DFA。代码片段不是我发明的

于 2020-11-07T00:24:11.697 回答