F# 函数的多次退出

发布于 2024-08-08 20:18:04 字数 1685 浏览 6 评论 0原文

我可以在 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 方法来处理这类事情?

I could do this easily in C++ (note: I didn't test this for correctness--it's only to illustrate what I'm trying to do):

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

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

      //normal processing

      return Success;
   }

But I cannot figure out how to exit a routine early in F#. What I want to do is to exit the function on a bad input but continue if the input is ok. Am I missing some fundamental property of F# or am I approaching the problem in the wrong way since I'm just learning FP?
Is a failwith my only option here?

This is what I've got so far and it compiles 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;;

Is there an FP way of dealing with this sort of thing?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

比忠 2024-08-15 20:18:04

在 F# 中,一切都由表达式组成(而在许多其他语言中,关键构建块是语句)。没有办法提前退出函数,但通常不需要这样做。在 C 中,有一个 if/else 块,其中分支由语句组成。在 F# 中,有一个 if/else 表达式,其中每个分支的计算结果为某种类型的值,整个 if/else 表达式的值为 1分支或其他。

所以这个 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 分支。

In F#, everything's made up of expressions (whereas in many other languages, the key building block is a statement). There's no way to exit a function early, but often this isn't needed. In C, you have an if/else blocks where the branches are made up of statements. In F#, there's an if/else expression, where each branch evaluates to a value of some type, and the value of the entire if/else expression is the value of one branch or the other.

So this C++:

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

Looks like this in F#:

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

Your code is on the right track, but you can refactor it, putting most of your logic in the else branch, with the "early return" logic in the if branch.

小ぇ时光︴ 2024-08-15 20:18:04

在我看来,匹配表达式类似于 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 中。在函数式语言中,函数的大小确实没有限制。

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

In my opinion, match expressions are the F# analogue of early-exit for calling out erroneous conditions and handling them separately. For your example, I'd write:

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

This separates the error case nicely. In general, if you need to exit from the middle of something, split functions and then put the error case in a match. There's really no limit to how small functions should be in a functional language.

As an aside, your use of discriminated unions as sets of integer constants is a little weird. If you like that idiom, be aware that you don't need to include the type name when referring to them.

情话墙 2024-08-15 20:18:04

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

也就是说,我当然可以看到哪里有足够的潜在退出点,长 if..then..else 链可能不是很长可读 - 尤其是在处理某些外部 API 时,这些 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,块的其余部分甚至不会被评估。如果块在没有返回的情况下“结束”流动,您将收到运行时异常(到目前为止,我没有看到任何方法可以在编译时强制执行此操作)。

这有一些限制。首先,工作流程确实不完整 - 它允许您使用 letuseifwhilefor 内部,但不是 try..withtry..finally。这是可以完成的 - 你需要实现 Block.TryWithBlock.TryFinally - 但到目前为止我找不到它们的文档,所以这需要一点一点猜测和更多时间。当我有更多时间时,我可能会回过头来添加它们。

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

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

First of all, as others have already noted, it's not "the F# way" (well, not FP way, really). Since you don't deal with statements, but only expressions, there isn't really anything to break out of. In general, this is treated by a nested chain of if..then..else statements.

That said, I can certainly see where there are enough potential exit points that a long if..then..else chain can be not very readable - especially so when dealing with some external API that's written to return error codes rather than throw exceptions on failures (say Win32 API, or some COM component), so you really need that error handling code. If so, it seems the way to do this in F# in particular would be to write a workflow for it.
Here's my first take at it:

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

Usage sample:

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

As you can see, it effectively defines all constructs in such a way that, as soon as return is encountered, the rest of the block is not even evaluated. If block flows "off the end" without a return, you'll get a runtime exception (I don't see any way to enforce this at compile-time so far).

This comes with some limitations. First of all, the workflow really isn't complete - it lets you use let, use, if, while and for inside, but not try..with or try..finally. It can be done - you need to implement Block.TryWith and Block.TryFinally - but I can't find the docs for them so far, so this will need a little bit of guessing and more time. I might come back to it later when I have more time, and add them.

Second, since workflows are really just syntactic sugar for a chain of function calls and lambdas - and, in particular, all your code is in lambdas - you cannot use let mutable inside the workflow. It's why I've used ref and ! in the sample code above, which is the general-purpose workaround.

Finally, there's the inevitable performance penalty because of all the lambda calls. Supposedly, F# is better at optimizing such things than, say C# (which just leaves everything as is in IL), and can inline stuff on IL level and do other tricks; but I don't know much about it, so the exact performance hit, if any, could only be determined by profiling.

孤云独去闲 2024-08-15 20:18:04

与 Pavel 类似的选项,但不需要您自己的工作流构建器,只需将代码块放在 seq 表达式中,并让它 yield 错误消息。然后在表达式之后,您只需调用 FirstOrDefault 即可获取第一条错误消息(或 null)。

由于序列表达式的计算是惰性的,这意味着它只会继续到第一个错误点(假设您从未在序列上调用除 FirstOrDefault 之外的任何内容)。如果没有错误,那么它就会一直运行到最后。因此,如果您这样做,您将能够将收益视为提前回报。

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

An option similar to Pavel's, but without needing your own workflow builder, is just to put your code block within a seq expression, and have it yield error messages. Then right after the expression, you just call FirstOrDefault to get the first error message (or null).

Since a sequence expression evaluates lazily, that means it'll only proceed to the point of the first error (assuming you never call anything but FirstOrDefault on the sequence). And if there's no error then it simply runs through to the end. So if you do it this way you'll be able to think of yield just like an early return.

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
原野 2024-08-15 20:18:04

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

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

This recursive Fibonacci function has two exit points:

let rec fib n =
  if n < 2 then 1 else fib (n-2) + fib(n-1);;
                ^      ^
眼泪也成诗 2024-08-15 20:18:04

我自己发现了以下方法。基本上,它生成可能退出的 seq,其中每个退出由 yield 生成,然后仅返回 seq 的 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)

让我们回顾一下输出:

normal processing
0
bad param detected
-1

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

I discovered the following approach for myself. Basically, it generates a seq of possible exits, where each exit is generated by yield, then takes Seq.head of the seq to return only the exit value that is calculated first. I am not sure can we call this approach "functional" to any extent. As well not sure how efficient is this approach and how the language laziness is used under the hood. Also not sure if it is of any need for the author after so many years. But some advantages of this way is that the code looks pretty much like it looked originally in non-functional language while using only a minimal set of intrinsic features only.

See below how would look the code of the first sample of the question:

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

Let's make some test calls:

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

Let's review the output:

normal processing
0
bad param detected
-1

Hope this idea is helpful for those who may got stuck with this. Any comment regarding my concerns above are welcome for this answer.

め可乐爱微笑 2024-08-15 20:18:04

我喜欢的是,没有人提到 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 函数

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

上面的代码是我个人的一些(无代码生成)词法分析器/解析器生成器,仅用于在词法分析器和解析器中构造DFA。代码片段不是我发明的

I like that in no way did any mentioned the F# type Result??

it is formally given by

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

This will fit your needs and are used when handling none-IO errors for performance boost.

a good example is a parser function

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

if you are not familiar with the |> operator it simple passe the result of the left side(or above) code to the argument on the right side, here the lambda function above

The implementation of the base case function are of the users choice as long as it conform to the function type. As you might notice is that it is a highly generic function representation of any map (function) which might can fail. This includes all functions that never fails.

The above code are some of my personal (none code generation) lexer/parser generator, only used to construct the DFA in the lexer and the parser. The code snip are not invented by me

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文